Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
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 Inedo.ProGet/Inedo.ProGet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ To learn how to use the library, see [Getting Started with Inedo.NuGet](https://
<ItemGroup>
<PackageReference Include="System.Linq.Async" Version="6.0.3" />
<PackageReference Include="Tomlyn" Version="0.19.0" />
<PackageReference Include="YamlDotNet" Version="16.2.1" />
</ItemGroup>
</Project>
7 changes: 6 additions & 1 deletion Inedo.ProGet/Scan/DependencyScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,14 @@ private static async Task<string> GetImplicitFileAsync(DependencyScannerType sca
else if (files.Count > 1)
throw new DependencyScannerException("Multiple project files found in directory. Specify which project file you would like to scan be using \"--input\" argument.");

files = await fileSystem.FindFilesAsync(fileName, "pnpm-lock.yaml", true, cancellationToken).ToListAsync(cancellationToken);
if (files.Count > 0)
return (scannerType: DependencyScannerType.Npm, filePath: files[0].FullName);

files = await fileSystem.FindFilesAsync(fileName, "package-lock.json", true, cancellationToken).ToListAsync(cancellationToken);
if(files.Count > 0)
return (scannerType: DependencyScannerType.Npm, filePath: fileName);

files = await fileSystem.FindFilesAsync(fileName, "Cargo.lock", true, cancellationToken).ToListAsync(cancellationToken);
if(files.Count > 0)
return (scannerType: DependencyScannerType.Cargo, filePath: fileName);
Expand All @@ -173,6 +177,7 @@ private static async Task<string> GetImplicitFileAsync(DependencyScannerType sca
".slnx" or ".sln" or ".csproj" => (DependencyScannerType.NuGet, fileName),
".toml" => (DependencyScannerType.Cargo, fileName),
".lock" => Path.GetFileName(fileName).Equals("Cargo.lock", StringComparison.OrdinalIgnoreCase) ? (DependencyScannerType.Cargo, fileName) : (DependencyScannerType.Composer, fileName),
".yaml" => Path.GetFileName(fileName).Equals("pnpm-lock.yaml", StringComparison.OrdinalIgnoreCase) ? (DependencyScannerType.Npm, fileName) : (DependencyScannerType.Auto, fileName),
".json" => Path.GetFileName(fileName).Equals("composer.json", StringComparison.OrdinalIgnoreCase) ? (DependencyScannerType.Composer, fileName) : (DependencyScannerType.Npm, fileName),
_ => Path.GetFileName(fileName).Equals("requirements.txt", StringComparison.OrdinalIgnoreCase) ? (getPythonScannerType(fileName), fileName) : (DependencyScannerType.Auto, fileName)
};
Expand Down
175 changes: 164 additions & 11 deletions Inedo.ProGet/Scan/NpmDependencyScanner.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json;
using YamlDotNet.RepresentationModel;

namespace Inedo.DependencyScan;

Expand All @@ -12,23 +13,58 @@ internal sealed class NpmDependencyScanner(CreateDependencyScannerArgs args) : D
public override async Task<IReadOnlyCollection<ScannedProject>> ResolveDependenciesAsync(CancellationToken cancellationToken = default)
{
var projects = new List<ScannedProject>();
var searchDirectory = (await this.FileSystem.FileExistsAsync(this.SourcePath, cancellationToken))
? this.FileSystem.GetDirectoryName(this.SourcePath)
: this.SourcePath;

await foreach (var packageLockFile in this.FileSystem.FindFilesAsync(searchDirectory, "package-lock.json", !this.SourcePath.EndsWith("package-lock.json"), cancellationToken))
// Handle pnpm lock file
if (this.SourcePath.EndsWith("pnpm-lock.yaml"))
{
if (this.packageLockOnly && packageLockFile.FullName.Contains("node_modules", StringComparison.OrdinalIgnoreCase))
continue;
if(!await this.FileSystem.FileExistsAsync(this.SourcePath, cancellationToken))
throw new FileNotFoundException("The specified pnpm lock file was not found.", this.SourcePath);
using var stream = await this.FileSystem.OpenReadAsync(this.SourcePath, cancellationToken).ConfigureAwait(false);
using var reader = new StreamReader(stream);

var yaml = new YamlStream();
yaml.Load(reader);

using var stream = await this.FileSystem.OpenReadAsync(packageLockFile.FullName, cancellationToken).ConfigureAwait(false);
using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
if (yaml.Documents.Count > 0 && yaml.Documents[0].RootNode is YamlMappingNode rootNode)
{
// Try to get project name from package.json in the same directory
var projectName = await GetProjectNameFromPackageJsonAsync(this.SourcePath, cancellationToken).ConfigureAwait(false);

if (string.IsNullOrEmpty(projectName))
throw new InvalidOperationException($"Unable to determine project name from package.json in directory: {this.FileSystem.GetDirectoryName(this.SourcePath)}");

var dependencies = ReadPnpmLockFile(rootNode).ToList();
projects.Add(new ScannedProject(projectName, dependencies));
}

var projectName = doc.RootElement.GetProperty("name").GetString()!;
projects.Add(new ScannedProject(projectName, ReadPackageLockFile(doc).Distinct()));
return projects;
}
// Handle standard npm package-lock.json files
else
{
var searchDirectory = (await this.FileSystem.FileExistsAsync(this.SourcePath, cancellationToken))
? this.FileSystem.GetDirectoryName(this.SourcePath)
: this.SourcePath;

if(await this.FileSystem.FindFilesAsync(searchDirectory, "pnpm-lock.yaml", true, cancellationToken).AnyAsync(cancellationToken: cancellationToken))
Console.WriteLine("Warning: pnpm-lock.yaml file detected in the scan directory. To parse pNPM lock files, specify the the pnpm-lock.yaml file in the Source Path argument.");

return projects;
await foreach (var packageLockFile in this.FileSystem.FindFilesAsync(searchDirectory, "package-lock.json", !this.SourcePath.EndsWith("package-lock.json"), cancellationToken))
{
if (this.packageLockOnly && packageLockFile.FullName.Contains("node_modules", StringComparison.OrdinalIgnoreCase))
continue;

using var stream = await this.FileSystem.OpenReadAsync(packageLockFile.FullName, cancellationToken).ConfigureAwait(false);
using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);

var projectName = doc.RootElement.GetProperty("name").GetString()!;
projects.Add(new ScannedProject(projectName, ReadPackageLockFile(doc).Distinct()));
}

return projects;
}


}

private IEnumerable<DependencyPackage> ReadPackageLockFile(JsonDocument doc)
Expand Down Expand Up @@ -132,6 +168,123 @@ private IEnumerable<DependencyPackage> ReadDependencies(JsonElement doc)
}
}

/// <summary>
/// Extrac the package name from the package.json file
/// </summary>
/// <param name="pnpmLockPath">pnpm package lock file path</param>
/// <param name="cancellationToken">Cancellation Tocken</param>
/// <returns>npm Project Name</returns>
/// <remarks>This should only be used when parsing pnpm lock files. pnpm does not store the package name in the lock file like npm does.</remarks>
private async Task<string?> GetProjectNameFromPackageJsonAsync(string pnpmLockPath, CancellationToken cancellationToken)
{
try
{
var directory = this.FileSystem.GetDirectoryName(pnpmLockPath);
var packageJsonPath = Path.Combine(directory, "package.json");

if (await this.FileSystem.FileExistsAsync(packageJsonPath, cancellationToken).ConfigureAwait(false))
{
using var stream = await this.FileSystem.OpenReadAsync(packageJsonPath, cancellationToken).ConfigureAwait(false);
using var doc = await System.Text.Json.JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);

if (doc.RootElement.TryGetProperty("name", out var nameProperty))
{
return nameProperty.GetString();
}
}
}
catch
{
// Ignore errors when reading package.json
// Later a meaningful exception will be thrown, when the project name is empty
}

return null;
}

/// <summary>
/// Read the pnpm lock file (pnpm-lock.yaml) file format for packages
/// </summary>
/// <param name="rootNode">Root Yaml Mapping Node</param>
/// <returns>A list of dependency pagkcages</returns>
private IEnumerable<DependencyPackage> ReadPnpmLockFile(YamlMappingNode rootNode)
{
// Find the "packages" node
var packagesKey = new YamlScalarNode("packages");
if (!rootNode.Children.TryGetValue(packagesKey, out var packagesNode) || packagesNode is not YamlMappingNode packagesMapping)
yield break;

foreach (var package in packagesMapping.Children)
{
if (package.Key is not YamlScalarNode keyNode || package.Value is not YamlMappingNode valueNode)
continue;

// Package key format in pnpm-lock.yaml is typically:
// - "/@scope/package@version" or "/package@version" for regular dependencies
// - "/@scope/package@version(peer-deps)" for packages with peer dependencies

var packageKey = keyNode.Value;

// Skip root package
if (string.IsNullOrEmpty(packageKey) || packageKey == ".")
continue;

// Remove leading slash if present
if (packageKey.StartsWith('/'))
packageKey = packageKey[1..];

// Check if it's a dev dependency
var devKey = new YamlScalarNode("dev");
if (valueNode.Children.TryGetValue(devKey, out var devNode) &&
devNode is YamlScalarNode devScalar &&
bool.TryParse(devScalar.Value, out var isDev) &&
isDev && !this.includeDevDependencies)
continue;

// Parse package name and version
var (name, version) = ParsePackageKey(packageKey);

if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(version))
{
yield return CreateNpmDependencyPackage(name, version);
}
}
}

/// <summary>
/// Parse the package key to extract name and version
/// </summary>
/// <param name="packageKey">pnpm package key</param>
/// <returns></returns>
private static (string name, string version) ParsePackageKey(string packageKey)
{
// Remove anything in parentheses (peer dependency info)
var parenIndex = packageKey.IndexOf('(');
if (parenIndex >= 0)
packageKey = packageKey[..parenIndex];

// Find the last @ symbol which separates name from version
// For scoped packages like @scope/[email protected], we need to find the @ after the scope
var lastAtIndex = packageKey.LastIndexOf('@');

if (lastAtIndex <= 0) // No version found or @ is at the start (scoped package without version)
return (packageKey, string.Empty);

// For scoped packages, make sure we don't split on the first @
if (packageKey.StartsWith('@'))
{
// This is a scoped package
var secondAtIndex = packageKey.IndexOf('@', 1);
if (secondAtIndex > 0)
lastAtIndex = secondAtIndex;
}

var name = packageKey[..lastAtIndex];
var version = packageKey[(lastAtIndex + 1)..];

return (name, version);
}

private static DependencyPackage CreateNpmDependencyPackage(string name, string version)
{
// Check for scoped name ("@scopename/packagename")
Expand Down