up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Symbols Server CI / symbols-smoke (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-24 20:57:49 +02:00
parent 46c8c47d06
commit 7c39058386
92 changed files with 3549 additions and 157 deletions

View File

@@ -625,7 +625,7 @@ internal static class NodePackageCollector
var lifecycleScripts = ExtractLifecycleScripts(root);
var nodeVersions = NodeVersionDetector.Detect(context, relativeDirectory, cancellationToken);
return new NodePackage(
var package = new NodePackage(
name: name.Trim(),
version: version.Trim(),
relativePath: relativeDirectory,
@@ -644,6 +644,10 @@ internal static class NodePackageCollector
lockLocator: lockLocator,
packageSha256: packageSha256,
isYarnPnp: yarnPnpPresent);
AttachEntrypoints(package, root, relativeDirectory);
return package;
}
private static string NormalizeRelativeDirectory(LanguageAnalyzerContext context, string directory)
@@ -825,4 +829,169 @@ internal static class NodePackageCollector
=> name.Equals("preinstall", StringComparison.OrdinalIgnoreCase)
|| name.Equals("install", StringComparison.OrdinalIgnoreCase)
|| name.Equals("postinstall", StringComparison.OrdinalIgnoreCase);
private static void AttachEntrypoints(LanguageAnalyzerContext context, NodePackage package, JsonElement root, string relativeDirectory)
{
static string NormalizePath(string relativeDirectory, string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
var normalized = path.Replace('\\', '/').Trim();
while (normalized.StartsWith("./", StringComparison.Ordinal))
{
normalized = normalized[2..];
}
normalized = normalized.TrimStart('/');
if (string.IsNullOrWhiteSpace(relativeDirectory))
{
return normalized;
}
return $"{relativeDirectory.TrimEnd('/')}/{normalized}";
}
void AddEntrypoint(string? path, string conditionSet, string? binName = null, string? mainField = null, string? moduleField = null)
{
var normalized = NormalizePath(relativeDirectory, path);
if (string.IsNullOrWhiteSpace(normalized))
{
return;
}
package.AddEntrypoint(normalized, conditionSet, binName, mainField, moduleField);
}
if (root.TryGetProperty("bin", out var binElement))
{
if (binElement.ValueKind == JsonValueKind.String)
{
AddEntrypoint(binElement.GetString(), string.Empty, binName: null);
}
else if (binElement.ValueKind == JsonValueKind.Object)
{
foreach (var prop in binElement.EnumerateObject())
{
if (prop.Value.ValueKind == JsonValueKind.String)
{
AddEntrypoint(prop.Value.GetString(), string.Empty, binName: prop.Name);
}
}
}
}
if (root.TryGetProperty("main", out var mainElement) && mainElement.ValueKind == JsonValueKind.String)
{
var mainField = mainElement.GetString();
AddEntrypoint(mainField, string.Empty, mainField: mainField);
}
if (root.TryGetProperty("module", out var moduleElement) && moduleElement.ValueKind == JsonValueKind.String)
{
var moduleField = moduleElement.GetString();
AddEntrypoint(moduleField, string.Empty, moduleField: moduleField);
}
if (root.TryGetProperty("exports", out var exportsElement))
{
foreach (var export in FlattenExports(exportsElement, prefix: string.Empty))
{
AddEntrypoint(export.Path, export.Conditions, binName: null, mainField: null, moduleField: null);
}
}
DetectShebangEntrypoints(context, package, relativeDirectory);
}
private static IEnumerable<(string Path, string Conditions)> FlattenExports(JsonElement element, string prefix)
{
switch (element.ValueKind)
{
case JsonValueKind.String:
var value = element.GetString();
if (!string.IsNullOrWhiteSpace(value))
{
yield return (value!, prefix);
}
yield break;
case JsonValueKind.Object:
foreach (var property in element.EnumerateObject())
{
var nextPrefix = string.IsNullOrWhiteSpace(prefix) ? property.Name : $"{prefix},{property.Name}";
foreach (var nested in FlattenExports(property.Value, nextPrefix))
{
yield return nested;
}
}
yield break;
default:
yield break;
}
}
private static void DetectShebangEntrypoints(LanguageAnalyzerContext context, NodePackage package, string relativeDirectory)
{
var baseDirectory = string.IsNullOrWhiteSpace(relativeDirectory)
? context.RootPath
: Path.Combine(context.RootPath, relativeDirectory.Replace('/', Path.DirectorySeparatorChar));
if (!Directory.Exists(baseDirectory))
{
return;
}
var candidates = Directory.EnumerateFiles(
baseDirectory,
"*.*",
new EnumerationOptions
{
RecurseSubdirectories = false,
MatchCasing = MatchCasing.CaseInsensitive,
IgnoreInaccessible = true
})
.Where(path =>
{
var ext = Path.GetExtension(path);
return string.Equals(ext, ".js", StringComparison.OrdinalIgnoreCase)
|| string.Equals(ext, ".mjs", StringComparison.OrdinalIgnoreCase)
|| string.Equals(ext, ".cjs", StringComparison.OrdinalIgnoreCase)
|| string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase);
})
.OrderBy(static p => p, StringComparer.Ordinal);
foreach (var file in candidates)
{
try
{
using var reader = File.OpenText(file);
var firstLine = reader.ReadLine();
if (string.IsNullOrWhiteSpace(firstLine))
{
continue;
}
if (!firstLine.TrimStart().StartsWith("#!", StringComparison.Ordinal))
{
continue;
}
if (!firstLine.Contains("node", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var relativePath = context.GetRelativePath(file).Replace(Path.DirectorySeparatorChar, '/');
package.AddEntrypoint(relativePath, conditionSet: "shebang:node", binName: null, mainField: null, moduleField: null);
}
catch (IOException)
{
// ignore unreadable files
}
}
}
}