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
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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/entry-demo@1.0.0",
|
||||
"purl": "pkg:npm/entry-demo@1.0.0",
|
||||
"name": "entry-demo",
|
||||
"version": "1.0.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"entrypoint": "bin/ed.js;cli.js;dist/feature.browser.js;dist/feature.node.js;dist/main.js;dist/module.mjs",
|
||||
"entrypoint.conditions": "browser;import;node;require",
|
||||
"path": "."
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "bin/ed.js;ed-alt"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "cli.js;entry-demo"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "dist/feature.browser.js;browser"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "dist/feature.node.js;node"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "dist/main.js;dist/main.js"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "dist/main.js;require"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "dist/module.mjs;dist/module.mjs"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "dist/module.mjs;import"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "entry-demo",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/main.js",
|
||||
"module": "dist/module.mjs",
|
||||
"bin": {
|
||||
"entry-demo": "cli.js",
|
||||
"ed-alt": "bin/ed.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/module.mjs",
|
||||
"require": "./dist/main.js"
|
||||
},
|
||||
"./feature": {
|
||||
"browser": "./dist/feature.browser.js",
|
||||
"node": "./dist/feature.node.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
[
|
||||
{
|
||||
"analyzerId": "node",
|
||||
"componentKey": "purl::pkg:npm/shebang-demo@0.1.0",
|
||||
"purl": "pkg:npm/shebang-demo@0.1.0",
|
||||
"name": "shebang-demo",
|
||||
"version": "0.1.0",
|
||||
"type": "npm",
|
||||
"usedByEntrypoint": false,
|
||||
"metadata": {
|
||||
"entrypoint": "run.js",
|
||||
"entrypoint.conditions": "shebang:node",
|
||||
"path": "."
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"kind": "file",
|
||||
"source": "package.json",
|
||||
"locator": "package.json"
|
||||
},
|
||||
{
|
||||
"kind": "metadata",
|
||||
"source": "package.json:entrypoint",
|
||||
"locator": "package.json#entrypoint",
|
||||
"value": "run.js;shebang:node"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "shebang-demo",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env node
|
||||
console.log('ok');
|
||||
@@ -100,4 +100,42 @@ public sealed class NodeLanguageAnalyzerTests
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EntrypointsAreCapturedAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "node", "entrypoints");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new NodeLanguageAnalyzer()
|
||||
};
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShebangEntrypointsAreCapturedAsync()
|
||||
{
|
||||
var cancellationToken = TestContext.Current.CancellationToken;
|
||||
var fixturePath = TestPaths.ResolveFixture("lang", "node", "shebang");
|
||||
var goldenPath = Path.Combine(fixturePath, "expected.json");
|
||||
|
||||
var analyzers = new ILanguageAnalyzer[]
|
||||
{
|
||||
new NodeLanguageAnalyzer()
|
||||
};
|
||||
|
||||
await LanguageAnalyzerTestHarness.AssertDeterministicAsync(
|
||||
fixturePath,
|
||||
goldenPath,
|
||||
analyzers,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user