diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmYaml.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmYaml.cs
new file mode 100644
index 000000000..8ce9c4e56
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmYaml.cs
@@ -0,0 +1,12 @@
+namespace Microsoft.ComponentDetection.Detectors.Pnpm;
+
+using YamlDotNet.Serialization;
+
+///
+/// Base class for all Pnpm lockfiles. Used for parsing the lockfile version.
+///
+public class PnpmYaml
+{
+ [YamlMember(Alias = "lockfileVersion")]
+ public string LockfileVersion { get; set; }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmYamlV5.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmYamlV5.cs
index 47f031a9b..d9fbe9a6f 100644
--- a/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmYamlV5.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmYamlV5.cs
@@ -6,14 +6,11 @@ namespace Microsoft.ComponentDetection.Detectors.Pnpm;
///
/// Format for a Pnpm lock file version 5 as defined in https://github.com/pnpm/spec/blob/master/lockfile/5.md.
///
-public class PnpmYamlV5
+public class PnpmYamlV5 : PnpmYaml
{
[YamlMember(Alias = "dependencies")]
public Dictionary Dependencies { get; set; }
[YamlMember(Alias = "packages")]
public Dictionary Packages { get; set; }
-
- [YamlMember(Alias = "lockfileVersion")]
- public string LockfileVersion { get; set; }
}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmHasDependenciesV6.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V6/PnpmHasDependenciesV6.cs
similarity index 91%
rename from src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmHasDependenciesV6.cs
rename to src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V6/PnpmHasDependenciesV6.cs
index e04135aa2..c1fd6891d 100644
--- a/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmHasDependenciesV6.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V6/PnpmHasDependenciesV6.cs
@@ -3,7 +3,7 @@ namespace Microsoft.ComponentDetection.Detectors.Pnpm;
using System.Collections.Generic;
using YamlDotNet.Serialization;
-public class PnpmHasDependenciesV6
+public class PnpmHasDependenciesV6 : PnpmYaml
{
[YamlMember(Alias = "dependencies")]
public Dictionary Dependencies { get; set; }
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmYamlV6.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V6/PnpmYamlV6.cs
similarity index 89%
rename from src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmYamlV6.cs
rename to src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V6/PnpmYamlV6.cs
index d9ca7bbf4..f71768081 100644
--- a/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmYamlV6.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V6/PnpmYamlV6.cs
@@ -17,7 +17,4 @@ public class PnpmYamlV6 : PnpmHasDependenciesV6
[YamlMember(Alias = "packages")]
public Dictionary Packages { get; set; }
-
- [YamlMember(Alias = "lockfileVersion")]
- public string LockfileVersion { get; set; }
}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmYamlV6Dependency.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V6/PnpmYamlV6Dependency.cs
similarity index 100%
rename from src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/PnpmYamlV6Dependency.cs
rename to src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V6/PnpmYamlV6Dependency.cs
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V9/PnpmHasDependenciesV9.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V9/PnpmHasDependenciesV9.cs
new file mode 100644
index 000000000..ebb91a305
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V9/PnpmHasDependenciesV9.cs
@@ -0,0 +1,16 @@
+namespace Microsoft.ComponentDetection.Detectors.Pnpm;
+
+using System.Collections.Generic;
+using YamlDotNet.Serialization;
+
+public class PnpmHasDependenciesV9 : PnpmYaml
+{
+ [YamlMember(Alias = "dependencies")]
+ public Dictionary Dependencies { get; set; }
+
+ [YamlMember(Alias = "devDependencies")]
+ public Dictionary DevDependencies { get; set; }
+
+ [YamlMember(Alias = "optionalDependencies")]
+ public Dictionary OptionalDependencies { get; set; }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V9/PnpmYamlV9.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V9/PnpmYamlV9.cs
new file mode 100644
index 000000000..44e077ee2
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V9/PnpmYamlV9.cs
@@ -0,0 +1,21 @@
+namespace Microsoft.ComponentDetection.Detectors.Pnpm;
+
+using System.Collections.Generic;
+using YamlDotNet.Serialization;
+
+///
+/// There is still no official docs for the new v9 lock if format, so these parsing contracts are empirically based.
+/// Issue tracking v9 specs: https://github.com/pnpm/spec/issues/6
+/// Format should eventually get updated here: https://github.com/pnpm/spec/blob/master/lockfile/6.0.md.
+///
+public class PnpmYamlV9 : PnpmHasDependenciesV9
+{
+ [YamlMember(Alias = "importers")]
+ public Dictionary Importers { get; set; }
+
+ [YamlMember(Alias = "packages")]
+ public Dictionary Packages { get; set; }
+
+ [YamlMember(Alias = "snapshots")]
+ public Dictionary Snapshots { get; set; }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V9/PnpmYamlV9Dependency.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V9/PnpmYamlV9Dependency.cs
new file mode 100644
index 000000000..eb7c35e07
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Contracts/V9/PnpmYamlV9Dependency.cs
@@ -0,0 +1,9 @@
+namespace Microsoft.ComponentDetection.Detectors.Pnpm;
+
+using YamlDotNet.Serialization;
+
+public class PnpmYamlV9Dependency
+{
+ [YamlMember(Alias = "version")]
+ public string Version { get; set; }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmParsingUtilitiesBase.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmParsingUtilitiesBase.cs
new file mode 100644
index 000000000..2c3dcd58b
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmParsingUtilitiesBase.cs
@@ -0,0 +1,52 @@
+namespace Microsoft.ComponentDetection.Detectors.Pnpm;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.ComponentDetection.Contracts;
+using YamlDotNet.Serialization;
+
+public abstract class PnpmParsingUtilitiesBase
+where T : PnpmYaml
+{
+ public T DeserializePnpmYamlFile(string fileContent)
+ {
+ var deserializer = new DeserializerBuilder()
+ .IgnoreUnmatchedProperties()
+ .Build();
+ return deserializer.Deserialize(new StringReader(fileContent));
+ }
+
+ public virtual bool IsPnpmPackageDevDependency(Package pnpmPackage)
+ {
+ ArgumentNullException.ThrowIfNull(pnpmPackage);
+
+ return string.Equals(bool.TrueString, pnpmPackage.Dev, StringComparison.InvariantCultureIgnoreCase);
+ }
+
+ public bool IsLocalDependency(KeyValuePair dependency)
+ {
+ // Local dependencies are dependencies that live in the file system
+ // this requires an extra parsing that is not supported yet
+ return dependency.Key.StartsWith("file:") || dependency.Value.StartsWith("file:") || dependency.Value.StartsWith("link:");
+ }
+
+ ///
+ /// Parse a pnpm path of the form "/package-name/version".
+ ///
+ /// a pnpm path of the form "/package-name/version".
+ /// Data parsed from path.
+ public abstract DetectedComponent CreateDetectedComponentFromPnpmPath(string pnpmPackagePath);
+
+ public virtual string ReconstructPnpmDependencyPath(string dependencyName, string dependencyVersion)
+ {
+ if (dependencyVersion.StartsWith('/'))
+ {
+ return dependencyVersion;
+ }
+ else
+ {
+ return $"/{dependencyName}@{dependencyVersion}";
+ }
+ }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmParsingUtilitiesFactory.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmParsingUtilitiesFactory.cs
new file mode 100644
index 000000000..53d53b129
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmParsingUtilitiesFactory.cs
@@ -0,0 +1,27 @@
+namespace Microsoft.ComponentDetection.Detectors.Pnpm;
+
+using System.IO;
+using YamlDotNet.Serialization;
+
+public static class PnpmParsingUtilitiesFactory
+{
+ public static PnpmParsingUtilitiesBase Create()
+ where T : PnpmYaml
+ {
+ return typeof(T).Name switch
+ {
+ nameof(PnpmYamlV5) => new PnpmV5ParsingUtilities(),
+ nameof(PnpmYamlV6) => new PnpmV6ParsingUtilities(),
+ nameof(PnpmYamlV9) => new PnpmV9ParsingUtilities(),
+ _ => new PnpmV5ParsingUtilities(),
+ };
+ }
+
+ public static string DeserializePnpmYamlFileVersion(string fileContent)
+ {
+ var deserializer = new DeserializerBuilder()
+ .IgnoreUnmatchedProperties()
+ .Build();
+ return deserializer.Deserialize(new StringReader(fileContent)).LockfileVersion;
+ }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmV5ParsingUtilities.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmV5ParsingUtilities.cs
new file mode 100644
index 000000000..02015c288
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmV5ParsingUtilities.cs
@@ -0,0 +1,58 @@
+namespace Microsoft.ComponentDetection.Detectors.Pnpm;
+
+using System.Linq;
+using global::NuGet.Versioning;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+
+public class PnpmV5ParsingUtilities : PnpmParsingUtilitiesBase
+where T : PnpmYaml
+{
+ public override DetectedComponent CreateDetectedComponentFromPnpmPath(string pnpmPackagePath)
+ {
+ var (parentName, parentVersion) = this.ExtractNameAndVersionFromPnpmPackagePath(pnpmPackagePath);
+ return new DetectedComponent(new NpmComponent(parentName, parentVersion));
+ }
+
+ private (string Name, string Version) ExtractNameAndVersionFromPnpmPackagePath(string pnpmPackagePath)
+ {
+ var pnpmComponentDefSections = pnpmPackagePath.Trim('/').Split('/');
+ (var packageVersion, var indexVersionIsAt) = this.GetPackageVersion(pnpmComponentDefSections);
+ if (indexVersionIsAt == -1)
+ {
+ // No version = not expected input
+ return (null, null);
+ }
+
+ var normalizedPackageName = string.Join("/", pnpmComponentDefSections.Take(indexVersionIsAt).ToArray());
+ return (normalizedPackageName, packageVersion);
+ }
+
+ private (string PackageVersion, int VersionIndex) GetPackageVersion(string[] pnpmComponentDefSections)
+ {
+ var indexVersionIsAt = -1;
+ var packageVersion = string.Empty;
+ var lastIndex = pnpmComponentDefSections.Length - 1;
+
+ // get version from packages with format /mute-stream/0.0.6
+ if (SemanticVersion.TryParse(pnpmComponentDefSections[lastIndex], out var _))
+ {
+ return (pnpmComponentDefSections[lastIndex], lastIndex);
+ }
+
+ // get version from packages with format /@babel/helper-compilation-targets/7.10.4_@babel+core@7.10.5
+ var lastComponentSplit = pnpmComponentDefSections[lastIndex].Split("_");
+ if (SemanticVersion.TryParse(lastComponentSplit[0], out var _))
+ {
+ return (lastComponentSplit[0], lastIndex);
+ }
+
+ // get version from packages with format /sinon-chai/2.8.0/chai@3.5.0+sinon@1.17.7
+ if (SemanticVersion.TryParse(pnpmComponentDefSections[lastIndex - 1], out var _))
+ {
+ return (pnpmComponentDefSections[lastIndex - 1], lastIndex - 1);
+ }
+
+ return (packageVersion, indexVersionIsAt);
+ }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmV6ParsingUtilities.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmV6ParsingUtilities.cs
new file mode 100644
index 000000000..b488246a5
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmV6ParsingUtilities.cs
@@ -0,0 +1,54 @@
+namespace Microsoft.ComponentDetection.Detectors.Pnpm;
+
+using System;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+
+public class PnpmV6ParsingUtilities : PnpmParsingUtilitiesBase
+where T : PnpmYaml
+{
+ public override DetectedComponent CreateDetectedComponentFromPnpmPath(string pnpmPackagePath)
+ {
+ /*
+ * The format is documented at https://github.com/pnpm/spec/blob/master/dependency-path.md.
+ * At the writing it does not seem to reflect changes which were made in lock file format v6:
+ * See https://github.com/pnpm/spec/issues/5.
+ */
+
+ // Strip parenthesized suffices from package. These hold peed dep related information that is unneeded here.
+ // An example of a dependency path with these: /webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.6.0)(webpack@5.89.0)
+ var fullPackageNameAndVersion = pnpmPackagePath.Split("(")[0];
+
+ var packageNameParts = fullPackageNameAndVersion.Split("@");
+
+ // If package name contains `@` this will reconstruct it:
+ var fullPackageName = string.Join("@", packageNameParts[..^1]);
+
+ // Version is section after last `@`.
+ var packageVersion = packageNameParts[^1];
+
+ // Check for leading `/` from pnpm.
+ if (!fullPackageName.StartsWith('/'))
+ {
+ throw new FormatException("Found pnpm dependency path not starting with `/`. This case is currently unhandled.");
+ }
+
+ // Strip leading `/`.
+ // It is unclear if real packages could have a name starting with `/`, so avoid `TrimStart` that just in case.
+ var normalizedPackageName = fullPackageName[1..];
+
+ return new DetectedComponent(new NpmComponent(normalizedPackageName, packageVersion));
+ }
+
+ public override string ReconstructPnpmDependencyPath(string dependencyName, string dependencyVersion)
+ {
+ if (dependencyVersion.StartsWith('/'))
+ {
+ return dependencyVersion;
+ }
+ else
+ {
+ return $"/{dependencyName}@{dependencyVersion}";
+ }
+ }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmV9ParsingUtilities.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmV9ParsingUtilities.cs
new file mode 100644
index 000000000..55a903b93
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/ParsingUtilities/PnpmV9ParsingUtilities.cs
@@ -0,0 +1,58 @@
+namespace Microsoft.ComponentDetection.Detectors.Pnpm;
+
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+
+public class PnpmV9ParsingUtilities : PnpmParsingUtilitiesBase
+where T : PnpmYaml
+{
+ public override DetectedComponent CreateDetectedComponentFromPnpmPath(string pnpmPackagePath)
+ {
+ /*
+ * The format is documented at https://github.com/pnpm/spec/blob/master/dependency-path.md.
+ * At the writing it does not seem to reflect changes which were made in lock file format v6:
+ * See https://github.com/pnpm/spec/issues/5.
+ */
+
+ // Strip parenthesized suffices from package. These hold peed dep related information that is unneeded here.
+ // An example of a dependency path with these: /webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.6.0)(webpack@5.89.0)
+ var fullPackageNameAndVersion = pnpmPackagePath.Split("(")[0];
+
+ var packageNameParts = fullPackageNameAndVersion.Split("@");
+
+ // If package name contains `@` this will reconstruct it:
+ var fullPackageName = string.Join("@", packageNameParts[..^1]);
+
+ // Version is section after last `@`.
+ var packageVersion = packageNameParts[^1];
+
+ return new DetectedComponent(new NpmComponent(fullPackageName, packageVersion));
+ }
+
+ public override bool IsPnpmPackageDevDependency(Package pnpmPackage)
+ {
+ // TODO: Implement
+ return false;
+ }
+
+ ///
+ /// Combine the information from a dependency edge in the dependency graph encoded in the ymal file into a full pnpm dependency path.
+ ///
+ /// The name of the dependency, as used as as the dictionary key in the yaml file when referring to the dependency.
+ /// The final resolved version of the package for this dependency edge.
+ /// This includes details like which version of specific dependencies were specified as peer dependencies.
+ /// In some edge cases, such as aliased packages, this version may be an absolute dependency path, but the leading slash has been removed.
+ /// leaving the "dependencyName" unused, which is checked by whether there is an @ in the version.
+ /// A pnpm dependency path for the specified version of the named package.
+ public override string ReconstructPnpmDependencyPath(string dependencyName, string dependencyVersion)
+ {
+ if (dependencyVersion.Contains('@'))
+ {
+ return dependencyVersion;
+ }
+ else
+ {
+ return $"{dependencyName}@{dependencyVersion}";
+ }
+ }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm5Detector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm5Detector.cs
index b91403ffe..9f38f0824 100644
--- a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm5Detector.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm5Detector.cs
@@ -7,10 +7,11 @@ namespace Microsoft.ComponentDetection.Detectors.Pnpm;
public class Pnpm5Detector : IPnpmDetector
{
public const string MajorVersion = "5";
+ private readonly PnpmParsingUtilitiesBase pnpmParsingUtilities = PnpmParsingUtilitiesFactory.Create();
public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileComponentRecorder singleFileComponentRecorder)
{
- var yaml = PnpmParsingUtilities.DeserializePnpmYamlV5File(yamlFileContent);
+ var yaml = this.pnpmParsingUtilities.DeserializePnpmYamlFile(yamlFileContent);
foreach (var packageKeyValue in yaml.Packages ?? Enumerable.Empty>())
{
@@ -20,8 +21,8 @@ public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileCom
continue;
}
- var parentDetectedComponent = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5(pnpmPackagePath: packageKeyValue.Key);
- var isDevDependency = packageKeyValue.Value != null && PnpmParsingUtilities.IsPnpmPackageDevDependency(packageKeyValue.Value);
+ var parentDetectedComponent = this.pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath(pnpmPackagePath: packageKeyValue.Key);
+ var isDevDependency = packageKeyValue.Value != null && this.pnpmParsingUtilities.IsPnpmPackageDevDependency(packageKeyValue.Value);
singleFileComponentRecorder.RegisterUsage(parentDetectedComponent, isDevelopmentDependency: isDevDependency);
parentDetectedComponent = singleFileComponentRecorder.GetComponent(parentDetectedComponent.Component.Id);
@@ -30,13 +31,13 @@ public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileCom
foreach (var dependency in packageKeyValue.Value.Dependencies)
{
// Ignore local packages.
- if (PnpmParsingUtilities.IsLocalDependency(dependency))
+ if (this.pnpmParsingUtilities.IsLocalDependency(dependency))
{
continue;
}
- var childDetectedComponent = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5(
- pnpmPackagePath: PnpmParsingUtilities.CreatePnpmPackagePathFromDependencyV5(dependency.Key, dependency.Value));
+ var childDetectedComponent = this.pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath(
+ pnpmPackagePath: this.CreatePnpmPackagePathFromDependency(dependency.Key, dependency.Value));
// Older code used the root's dev dependency value. We're leaving this null until we do a second pass to look at each components' top level referrers.
singleFileComponentRecorder.RegisterUsage(childDetectedComponent, parentComponentId: parentDetectedComponent.Component.Id, isDevelopmentDependency: null);
@@ -55,4 +56,9 @@ public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileCom
}
}
}
+
+ private string CreatePnpmPackagePathFromDependency(string dependencyName, string dependencyVersion)
+ {
+ return dependencyVersion.Contains('/') ? dependencyVersion : $"/{dependencyName}/{dependencyVersion}";
+ }
}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs
index 0a3e5a99e..10675f538 100644
--- a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm6Detector.cs
@@ -7,10 +7,11 @@ namespace Microsoft.ComponentDetection.Detectors.Pnpm;
public class Pnpm6Detector : IPnpmDetector
{
public const string MajorVersion = "6";
+ private readonly PnpmParsingUtilitiesBase pnpmParsingUtilities = PnpmParsingUtilitiesFactory.Create();
public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileComponentRecorder singleFileComponentRecorder)
{
- var yaml = PnpmParsingUtilities.DeserializePnpmYamlV6File(yamlFileContent);
+ var yaml = this.pnpmParsingUtilities.DeserializePnpmYamlFile(yamlFileContent);
// There may be multiple instance of the same package (even at the same version) in pnpm differentiated by other aspects of the pnpm dependency path.
// Therefor all DetectedComponents are tracked by the same full string pnpm uses, the pnpm dependency path, which is used as the key in this dictionary.
@@ -29,14 +30,14 @@ public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileCom
continue;
}
- var parentDetectedComponent = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV6(pnpmDependencyPath: pnpmDependencyPath);
+ var parentDetectedComponent = this.pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath(pnpmPackagePath: pnpmDependencyPath);
components.Add(pnpmDependencyPath, (parentDetectedComponent, package));
// Register the component.
// It should get registered again with with additional information (what depended on it) later,
// but registering it now ensures nothing is missed due to a limitation in dependency traversal
// like skipping local dependencies which might have transitively depended on this.
- singleFileComponentRecorder.RegisterUsage(parentDetectedComponent, isDevelopmentDependency: PnpmParsingUtilities.IsPnpmPackageDevDependency(package));
+ singleFileComponentRecorder.RegisterUsage(parentDetectedComponent, isDevelopmentDependency: this.pnpmParsingUtilities.IsPnpmPackageDevDependency(package));
}
// Now that the `components` dictionary is populated, make a second pass registering all the dependency edges in the graph.
@@ -44,7 +45,7 @@ public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileCom
{
foreach (var (name, version) in package.Dependencies ?? Enumerable.Empty>())
{
- var pnpmDependencyPath = PnpmParsingUtilities.ReconstructPnpmDependencyPathV6(name, version);
+ var pnpmDependencyPath = this.pnpmParsingUtilities.ReconstructPnpmDependencyPath(name, version);
// If this lookup fails, then pnpmDependencyPath was either parsed incorrectly or constructed incorrectly.
var (referenced, _) = components[pnpmDependencyPath];
@@ -82,12 +83,12 @@ private void ProcessDependencyList(ISingleFileComponentRecorder singleFileCompon
continue;
}
- var pnpmDependencyPath = PnpmParsingUtilities.ReconstructPnpmDependencyPathV6(name, dep.Version);
+ var pnpmDependencyPath = this.pnpmParsingUtilities.ReconstructPnpmDependencyPath(name, dep.Version);
var (component, package) = components[pnpmDependencyPath];
// Determine isDevelopmentDependency using metadata on package from pnpm rather than from which dependency list this package is under.
// This ensures that dependencies which are a direct dev dependency and an indirect non-dev dependency get listed as non-dev.
- var isDevelopmentDependency = PnpmParsingUtilities.IsPnpmPackageDevDependency(package);
+ var isDevelopmentDependency = this.pnpmParsingUtilities.IsPnpmPackageDevDependency(package);
singleFileComponentRecorder.RegisterUsage(component, isExplicitReferencedDependency: true, isDevelopmentDependency: isDevelopmentDependency);
}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm9Detector.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm9Detector.cs
new file mode 100644
index 000000000..b7828b1ee
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/Pnpm9Detector.cs
@@ -0,0 +1,102 @@
+namespace Microsoft.ComponentDetection.Detectors.Pnpm;
+
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.ComponentDetection.Contracts;
+
+///
+/// There is still no official docs for the new v9 lock if format, so these parsing contracts are empirically based.
+/// Issue tracking v9 specs: https://github.com/pnpm/spec/issues/6
+/// Format should eventually get updated here: https://github.com/pnpm/spec/blob/master/lockfile/6.0.md.
+///
+public class Pnpm9Detector : IPnpmDetector
+{
+ public const string MajorVersion = "9";
+ private readonly PnpmParsingUtilitiesBase pnpmParsingUtilities = PnpmParsingUtilitiesFactory.Create();
+
+ public void RecordDependencyGraphFromFile(string yamlFileContent, ISingleFileComponentRecorder singleFileComponentRecorder)
+ {
+ var yaml = this.pnpmParsingUtilities.DeserializePnpmYamlFile(yamlFileContent);
+
+ // There may be multiple instance of the same package (even at the same version) in pnpm differentiated by other aspects of the pnpm dependency path.
+ // Therefor all DetectedComponents are tracked by the same full string pnpm uses, the pnpm dependency path, which is used as the key in this dictionary.
+ // Some documentation about pnpm dependency paths can be found at https://github.com/pnpm/spec/blob/master/dependency-path.md.
+ var components = new Dictionary();
+
+ // Create a component for every package referenced in the lock file.
+ // This includes all directly and transitively referenced dependencies.
+ foreach (var (pnpmDependencyPath, package) in yaml.Snapshots ?? Enumerable.Empty>())
+ {
+ // Ignore "file:" as these are local packages.
+ // Such local packages should only be referenced at the top level (via ProcessDependencyList) which also skips them or from other local packages (which this skips).
+ // There should be no cases where a non-local package references a local package, so skipping them here should not result in failed lookups below when adding all the graph references.
+ if (pnpmDependencyPath.StartsWith("file:"))
+ {
+ continue;
+ }
+
+ var parentDetectedComponent = this.pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath(pnpmPackagePath: pnpmDependencyPath);
+ components.Add(pnpmDependencyPath, (parentDetectedComponent, package));
+
+ // Register the component.
+ // It should get registered again with with additional information (what depended on it) later,
+ // but registering it now ensures nothing is missed due to a limitation in dependency traversal
+ // like skipping local dependencies which might have transitively depended on this.
+ singleFileComponentRecorder.RegisterUsage(parentDetectedComponent);
+ }
+
+ // now that the components dictionary is populated, add direct dependencies of the current file/project setting isExplicitReferencedDependency to true
+ // during this step, recursively processes any indirect dependencies
+ foreach (var (_, package) in yaml.Importers ?? Enumerable.Empty>())
+ {
+ this.ProcessDependencySets(singleFileComponentRecorder, components, package);
+ }
+ }
+
+ private void ProcessDependencySets(ISingleFileComponentRecorder singleFileComponentRecorder, Dictionary components, PnpmHasDependenciesV9 item)
+ {
+ this.ProcessDependencyList(singleFileComponentRecorder, components, item.Dependencies, isDevelopmentDependency: false);
+ this.ProcessDependencyList(singleFileComponentRecorder, components, item.DevDependencies, isDevelopmentDependency: true);
+ this.ProcessDependencyList(singleFileComponentRecorder, components, item.OptionalDependencies, false);
+ }
+
+ private void ProcessDependencyList(ISingleFileComponentRecorder singleFileComponentRecorder, Dictionary components, Dictionary dependencies, bool isDevelopmentDependency)
+ {
+ foreach (var (name, dep) in dependencies ?? Enumerable.Empty>())
+ {
+ // Ignore "file:" and "link:" as these are local packages.
+ if (dep.Version.StartsWith("link:") || dep.Version.StartsWith("file:"))
+ {
+ continue;
+ }
+
+ var pnpmDependencyPath = this.pnpmParsingUtilities.ReconstructPnpmDependencyPath(name, dep.Version);
+ var (component, package) = components[pnpmDependencyPath];
+
+ // Lockfile v9 apparently removed the tagging of dev dependencies in the lockfile, so we revert to using the dependency tree to establish dev dependency state.
+ // At this point, the root dependencies are marked according to which dependency group they are declared in the lockfile itself.
+ singleFileComponentRecorder.RegisterUsage(component, isExplicitReferencedDependency: true, isDevelopmentDependency: isDevelopmentDependency);
+ this.ProcessIndirectDependencies(singleFileComponentRecorder, components, component.Component.Id, package.Dependencies, isDevelopmentDependency);
+ }
+ }
+
+ private void ProcessIndirectDependencies(ISingleFileComponentRecorder singleFileComponentRecorder, Dictionary components, string parentComponentId, Dictionary dependencies, bool isDevDependency)
+ {
+ // Now that the `components` dictionary is populated, make another pass of all components, registering all the dependency edges in the graph.
+ foreach (var (name, version) in dependencies ?? Enumerable.Empty>())
+ {
+ // Ignore "file:" and "link:" as these are local packages.
+ if (version.StartsWith("link:") || version.StartsWith("file:"))
+ {
+ continue;
+ }
+
+ var pnpmDependencyPath = this.pnpmParsingUtilities.ReconstructPnpmDependencyPath(name, version);
+
+ // If this lookup fails, then pnpmDependencyPath was either parsed incorrectly or constructed incorrectly.
+ var (component, package) = components[pnpmDependencyPath];
+ singleFileComponentRecorder.RegisterUsage(component, parentComponentId: parentComponentId, isExplicitReferencedDependency: false, isDevelopmentDependency: isDevDependency);
+ this.ProcessIndirectDependencies(singleFileComponentRecorder, components, component.Component.Id, package.Dependencies, isDevDependency);
+ }
+ }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs
index a4cd8824b..dca8dd2da 100644
--- a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs
+++ b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmComponentDetectorFactory.cs
@@ -98,7 +98,7 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID
private IPnpmDetector GetPnpmComponentDetector(string fileContent, out string detectedVersion)
{
- detectedVersion = PnpmParsingUtilities.DeserializePnpmYamlFileVersion(fileContent);
+ detectedVersion = PnpmParsingUtilitiesFactory.DeserializePnpmYamlFileVersion(fileContent);
this.RecordLockfileVersion(detectedVersion);
var majorVersion = detectedVersion?.Split(".")[0];
return majorVersion switch
@@ -110,6 +110,7 @@ private IPnpmDetector GetPnpmComponentDetector(string fileContent, out string de
null => new Pnpm5Detector(),
Pnpm5Detector.MajorVersion => new Pnpm5Detector(),
Pnpm6Detector.MajorVersion => new Pnpm6Detector(),
+ Pnpm9Detector.MajorVersion => new Pnpm9Detector(),
_ => null,
};
}
diff --git a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmParsingUtilities.cs b/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmParsingUtilities.cs
deleted file mode 100644
index fe107d9ec..000000000
--- a/src/Microsoft.ComponentDetection.Detectors/pnpm/PnpmParsingUtilities.cs
+++ /dev/null
@@ -1,167 +0,0 @@
-namespace Microsoft.ComponentDetection.Detectors.Pnpm;
-
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using global::NuGet.Versioning;
-using Microsoft.ComponentDetection.Contracts;
-using Microsoft.ComponentDetection.Contracts.TypedComponent;
-using YamlDotNet.Serialization;
-
-public static class PnpmParsingUtilities
-{
- public static string DeserializePnpmYamlFileVersion(string fileContent)
- {
- var deserializer = new DeserializerBuilder()
- .IgnoreUnmatchedProperties()
- .Build();
- return deserializer.Deserialize(new StringReader(fileContent)).LockfileVersion;
- }
-
- public static bool IsPnpmPackageDevDependency(Package pnpmPackage)
- {
- ArgumentNullException.ThrowIfNull(pnpmPackage);
-
- return string.Equals(bool.TrueString, pnpmPackage.Dev, StringComparison.InvariantCultureIgnoreCase);
- }
-
- public static PnpmYamlV5 DeserializePnpmYamlV5File(string fileContent)
- {
- var deserializer = new DeserializerBuilder()
- .IgnoreUnmatchedProperties()
- .Build();
- return deserializer.Deserialize(new StringReader(fileContent));
- }
-
- public static PnpmYamlV6 DeserializePnpmYamlV6File(string fileContent)
- {
- var deserializer = new DeserializerBuilder()
- .IgnoreUnmatchedProperties()
- .Build();
- return deserializer.Deserialize(new StringReader(fileContent));
- }
-
- public static bool IsLocalDependency(KeyValuePair dependency)
- {
- // Local dependencies are dependencies that live in the file system
- // this requires an extra parsing that is not supported yet
- return dependency.Key.StartsWith("file:") || dependency.Value.StartsWith("file:") || dependency.Value.StartsWith("link:");
- }
-
- ///
- /// Parse a pnpm path of the form "/package-name/version".
- ///
- /// a pnpm path of the form "/package-name/version".
- /// Data parsed from path.
- public static DetectedComponent CreateDetectedComponentFromPnpmPathV5(string pnpmPackagePath)
- {
- var (parentName, parentVersion) = ExtractNameAndVersionFromPnpmPackagePathV5(pnpmPackagePath);
- return new DetectedComponent(new NpmComponent(parentName, parentVersion));
- }
-
- public static string CreatePnpmPackagePathFromDependencyV5(string dependencyName, string dependencyVersion)
- {
- return dependencyVersion.Contains('/') ? dependencyVersion : $"/{dependencyName}/{dependencyVersion}";
- }
-
- private static (string Name, string Version) ExtractNameAndVersionFromPnpmPackagePathV5(string pnpmPackagePath)
- {
- var pnpmComponentDefSections = pnpmPackagePath.Trim('/').Split('/');
- (var packageVersion, var indexVersionIsAt) = GetPackageVersionV5(pnpmComponentDefSections);
- if (indexVersionIsAt == -1)
- {
- // No version = not expected input
- return (null, null);
- }
-
- var normalizedPackageName = string.Join("/", pnpmComponentDefSections.Take(indexVersionIsAt).ToArray());
- return (normalizedPackageName, packageVersion);
- }
-
- private static (string PackageVersion, int VersionIndex) GetPackageVersionV5(string[] pnpmComponentDefSections)
- {
- var indexVersionIsAt = -1;
- var packageVersion = string.Empty;
- var lastIndex = pnpmComponentDefSections.Length - 1;
-
- // get version from packages with format /mute-stream/0.0.6
- if (SemanticVersion.TryParse(pnpmComponentDefSections[lastIndex], out var _))
- {
- return (pnpmComponentDefSections[lastIndex], lastIndex);
- }
-
- // get version from packages with format /@babel/helper-compilation-targets/7.10.4_@babel+core@7.10.5
- var lastComponentSplit = pnpmComponentDefSections[lastIndex].Split("_");
- if (SemanticVersion.TryParse(lastComponentSplit[0], out var _))
- {
- return (lastComponentSplit[0], lastIndex);
- }
-
- // get version from packages with format /sinon-chai/2.8.0/chai@3.5.0+sinon@1.17.7
- if (SemanticVersion.TryParse(pnpmComponentDefSections[lastIndex - 1], out var _))
- {
- return (pnpmComponentDefSections[lastIndex - 1], lastIndex - 1);
- }
-
- return (packageVersion, indexVersionIsAt);
- }
-
- ///
- /// Parse a pnpm dependency path.
- ///
- /// A pnpm dependency path of the form "/@optional-scope/package-name@version(optional-ignored-data)(optional-ignored-data)".
- /// Data parsed from path.
- public static DetectedComponent CreateDetectedComponentFromPnpmPathV6(string pnpmDependencyPath)
- {
- /*
- * The format is documented at https://github.com/pnpm/spec/blob/master/dependency-path.md.
- * At the writing it does not seem to reflect changes which were made in lock file format v6:
- * See https://github.com/pnpm/spec/issues/5.
- */
-
- // Strip parenthesized suffices from package. These hold peed dep related information that is unneeded here.
- // An example of a dependency path with these: /webpack-cli@4.10.0(webpack-bundle-analyzer@4.10.1)(webpack-dev-server@4.6.0)(webpack@5.89.0)
- var fullPackageNameAndVersion = pnpmDependencyPath.Split("(")[0];
-
- var packageNameParts = fullPackageNameAndVersion.Split("@");
-
- // If package name contains `@` this will reconstruct it:
- var fullPackageName = string.Join("@", packageNameParts[..^1]);
-
- // Version is section after last `@`.
- var packageVersion = packageNameParts[^1];
-
- // Check for leading `/` from pnpm.
- if (!fullPackageName.StartsWith('/'))
- {
- throw new FormatException("Found pnpm dependency path not starting with `/`. This case is currently unhandled.");
- }
-
- // Strip leading `/`.
- // It is unclear if real packages could have a name starting with `/`, so avoid `TrimStart` that just in case.
- var normalizedPackageName = fullPackageName[1..];
-
- return new DetectedComponent(new NpmComponent(normalizedPackageName, packageVersion));
- }
-
- ///
- /// Combine the information from a dependency edge in the dependency graph encoded in the ymal file into a full pnpm dependency path.
- ///
- /// The name of the dependency, as used as as the dictionary key in the yaml file when referring to the dependency.
- /// The final resolved version of the package for this dependency edge.
- /// This includes details like which version of specific dependencies were specified as peer dependencies.
- /// In some edge cases, such as aliased packages, this version may be an absolute dependency path (starts with a slash) leaving the "dependencyName" unused.
- /// A pnpm dependency path for the specified version of the named package.
- public static string ReconstructPnpmDependencyPathV6(string dependencyName, string dependencyVersion)
- {
- if (dependencyVersion.StartsWith('/'))
- {
- return dependencyVersion;
- }
- else
- {
- return $"/{dependencyName}@{dependencyVersion}";
- }
- }
-}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs
index 6f94f34aa..3e73a2b33 100644
--- a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmDetectorTests.cs
@@ -332,10 +332,12 @@ public async Task TestPnpmDetector_DependencyGraphIsCreatedAsync()
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
componentRecorder.GetDetectedComponents().Should().HaveCount(4);
- var queryStringComponentId = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5("/query-string/4.3.4").Component.Id;
- var objectAssignComponentId = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5("/object-assign/4.1.1").Component.Id;
- var strictUriComponentId = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5("/strict-uri-encode/1.1.0").Component.Id;
- var testComponentId = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5("/test/1.0.0").Component.Id;
+ var pnpmParsingUtilities = PnpmParsingUtilitiesFactory.Create();
+
+ var queryStringComponentId = pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/query-string/4.3.4").Component.Id;
+ var objectAssignComponentId = pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/object-assign/4.1.1").Component.Id;
+ var strictUriComponentId = pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/strict-uri-encode/1.1.0").Component.Id;
+ var testComponentId = pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/test/1.0.0").Component.Id;
var dependencyGraph = componentRecorder.GetDependencyGraphsByLocation().Values.First();
@@ -380,8 +382,10 @@ public async Task TestPnpmDetector_DependenciesRefeToLocalPaths_DependenciesAreI
scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
componentRecorder.GetDetectedComponents().Should().HaveCount(2, "Components that comes from a file (file:* or link:*) should be ignored.");
- var queryStringComponentId = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5("/query-string/4.3.4").Component.Id;
- var nthcheck = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5("/nth-check/2.0.0").Component.Id;
+ var pnpmParsingUtilities = PnpmParsingUtilitiesFactory.Create();
+
+ var queryStringComponentId = pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/query-string/4.3.4").Component.Id;
+ var nthcheck = pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/nth-check/2.0.0").Component.Id;
var dependencyGraph = componentRecorder.GetDependencyGraphsByLocation().Values.First();
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmParsingUtilitiesTest.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmParsingUtilitiesTest.cs
index ba508d5a5..2d2f0ac65 100644
--- a/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmParsingUtilitiesTest.cs
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PnpmParsingUtilitiesTest.cs
@@ -35,10 +35,10 @@ public void DeserializePnpmYamlFileV3()
registry: 'https://test/registry'
shrinkwrapMinorVersion: 7
shrinkwrapVersion: 3";
-
- var version = PnpmParsingUtilities.DeserializePnpmYamlFileVersion(yamlFile);
+ var pnpmParsingUtilities = PnpmParsingUtilitiesFactory.Create();
+ var version = pnpmParsingUtilities.DeserializePnpmYamlFileVersion(yamlFile);
version.Should().BeNull(); // Versions older than 5 report null as they don't use the same version field.
- var parsedYaml = PnpmParsingUtilities.DeserializePnpmYamlV5File(yamlFile);
+ var parsedYaml = pnpmParsingUtilities.DeserializePnpmYamlFile(yamlFile);
parsedYaml.Packages.Should().HaveCount(2);
parsedYaml.Packages.Should().ContainKey("/query-string/4.3.4");
@@ -58,19 +58,20 @@ public void DeserializePnpmYamlFileV3()
[TestMethod]
public void CreateDetectedComponentFromPnpmPathV5()
{
- var detectedComponent1 = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5("/@ms/items-view/0.128.9/react-dom@15.6.2+react@15.6.2");
+ var pnpmParsingUtilities = PnpmParsingUtilitiesFactory.Create();
+ var detectedComponent1 = pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/@ms/items-view/0.128.9/react-dom@15.6.2+react@15.6.2");
detectedComponent1.Should().NotBeNull();
detectedComponent1.Component.Should().NotBeNull();
((NpmComponent)detectedComponent1.Component).Name.Should().BeEquivalentTo("@ms/items-view");
((NpmComponent)detectedComponent1.Component).Version.Should().BeEquivalentTo("0.128.9");
- var detectedComponent2 = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5("/@babel/helper-compilation-targets/7.10.4_@babel+core@7.10.5");
+ var detectedComponent2 = pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/@babel/helper-compilation-targets/7.10.4_@babel+core@7.10.5");
detectedComponent2.Should().NotBeNull();
detectedComponent2.Component.Should().NotBeNull();
((NpmComponent)detectedComponent2.Component).Name.Should().BeEquivalentTo("@babel/helper-compilation-targets");
((NpmComponent)detectedComponent2.Component).Version.Should().BeEquivalentTo("7.10.4");
- var detectedComponent3 = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV5("/query-string/4.3.4");
+ var detectedComponent3 = pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/query-string/4.3.4");
detectedComponent3.Should().NotBeNull();
detectedComponent3.Component.Should().NotBeNull();
((NpmComponent)detectedComponent3.Component).Name.Should().BeEquivalentTo("query-string");
@@ -85,24 +86,26 @@ public void IsPnpmPackageDevDependency()
Dev = "true",
};
- PnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeTrue();
+ var pnpmParsingUtilities = PnpmParsingUtilitiesFactory.Create();
+
+ pnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeTrue();
pnpmPackage.Dev = "TRUE";
- PnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeTrue();
+ pnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeTrue();
pnpmPackage.Dev = "false";
- PnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeFalse();
+ pnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeFalse();
pnpmPackage.Dev = "FALSE";
- PnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeFalse();
+ pnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeFalse();
pnpmPackage.Dev = string.Empty;
- PnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeFalse();
+ pnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeFalse();
pnpmPackage.Dev = null;
- PnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeFalse();
+ pnpmParsingUtilities.IsPnpmPackageDevDependency(pnpmPackage).Should().BeFalse();
- Action action = () => PnpmParsingUtilities.IsPnpmPackageDevDependency(null);
+ Action action = () => pnpmParsingUtilities.IsPnpmPackageDevDependency(null);
action.Should().Throw();
}
@@ -123,10 +126,10 @@ public void DeserializePnpmYamlFileV6()
/minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: false";
-
- var version = PnpmParsingUtilities.DeserializePnpmYamlFileVersion(yamlFile);
+ var pnpmParsingUtilities = PnpmParsingUtilitiesFactory.Create();
+ var version = PnpmParsingUtilitiesFactory.DeserializePnpmYamlFileVersion(yamlFile);
version.Should().Be("6.0");
- var parsedYaml = PnpmParsingUtilities.DeserializePnpmYamlV6File(yamlFile);
+ var parsedYaml = pnpmParsingUtilities.DeserializePnpmYamlFile(yamlFile);
parsedYaml.Packages.Should().ContainSingle();
parsedYaml.Packages.Should().ContainKey("/minimist@1.2.8");
@@ -142,23 +145,25 @@ public void DeserializePnpmYamlFileV6()
[TestMethod]
public void CreateDetectedComponentFromPnpmPathV6()
{
+ var pnpmParsingUtilities = PnpmParsingUtilitiesFactory.Create();
+
// Simple case: no scope, simple version
- var simple = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV6("/sort-scripts@1.0.1");
+ var simple = pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/sort-scripts@1.0.1");
((NpmComponent)simple.Component).Name.Should().BeEquivalentTo("sort-scripts");
((NpmComponent)simple.Component).Version.Should().BeEquivalentTo("1.0.1");
// With scope:
- var scoped = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV6("/@babel/eslint-parser@7.23.3");
+ var scoped = pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/@babel/eslint-parser@7.23.3");
((NpmComponent)scoped.Component).Name.Should().BeEquivalentTo("@babel/eslint-parser");
((NpmComponent)scoped.Component).Version.Should().BeEquivalentTo("7.23.3");
// With peer deps:
- var withPeerDeps = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV6("/mocha-json-output-reporter@2.1.0(mocha@10.2.0)(moment@2.29.4)");
+ var withPeerDeps = pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/mocha-json-output-reporter@2.1.0(mocha@10.2.0)(moment@2.29.4)");
((NpmComponent)withPeerDeps.Component).Name.Should().BeEquivalentTo("mocha-json-output-reporter");
((NpmComponent)withPeerDeps.Component).Version.Should().BeEquivalentTo("2.1.0");
// With everything:
- var complex = PnpmParsingUtilities.CreateDetectedComponentFromPnpmPathV6("/@babel/eslint-parser@7.23.3(@babel/core@7.23.3)(eslint@8.55.0)");
+ var complex = pnpmParsingUtilities.CreateDetectedComponentFromPnpmPath("/@babel/eslint-parser@7.23.3(@babel/core@7.23.3)(eslint@8.55.0)");
((NpmComponent)complex.Component).Name.Should().BeEquivalentTo("@babel/eslint-parser");
((NpmComponent)complex.Component).Version.Should().BeEquivalentTo("7.23.3");
}
@@ -166,16 +171,18 @@ public void CreateDetectedComponentFromPnpmPathV6()
[TestMethod]
public void ReconstructPnpmDependencyPathV6()
{
+ var pnpmParsingUtilities = PnpmParsingUtilitiesFactory.Create();
+
// Simple case: no scope, simple version
- PnpmParsingUtilities.ReconstructPnpmDependencyPathV6("sort-scripts", "1.0.1").Should().BeEquivalentTo("/sort-scripts@1.0.1");
+ pnpmParsingUtilities.ReconstructPnpmDependencyPath("sort-scripts", "1.0.1").Should().BeEquivalentTo("/sort-scripts@1.0.1");
// With scope:
- PnpmParsingUtilities.ReconstructPnpmDependencyPathV6("@babel/eslint-parser", "7.23.3").Should().BeEquivalentTo("/@babel/eslint-parser@7.23.3");
+ pnpmParsingUtilities.ReconstructPnpmDependencyPath("@babel/eslint-parser", "7.23.3").Should().BeEquivalentTo("/@babel/eslint-parser@7.23.3");
// With peer deps:
- PnpmParsingUtilities.ReconstructPnpmDependencyPathV6("mocha-json-output-reporter", "2.1.0(mocha@10.2.0)(moment@2.29.4)").Should().BeEquivalentTo("/mocha-json-output-reporter@2.1.0(mocha@10.2.0)(moment@2.29.4)");
+ pnpmParsingUtilities.ReconstructPnpmDependencyPath("mocha-json-output-reporter", "2.1.0(mocha@10.2.0)(moment@2.29.4)").Should().BeEquivalentTo("/mocha-json-output-reporter@2.1.0(mocha@10.2.0)(moment@2.29.4)");
// Absolute path:
- PnpmParsingUtilities.ReconstructPnpmDependencyPathV6("events_pkg", "/events@3.3.0").Should().BeEquivalentTo("/events@3.3.0");
+ pnpmParsingUtilities.ReconstructPnpmDependencyPath("events_pkg", "/events@3.3.0").Should().BeEquivalentTo("/events@3.3.0");
}
}