@cdxgen/cdxgen 13.0.1 → 13.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -16
- package/bin/audit.js +6 -0
- package/bin/cdxgen.js +141 -7
- package/bin/evinse.js +3 -1
- package/data/component-tags.json +8 -1
- package/data/crypto-oid.json +88 -12
- package/data/frameworks-list.json +86 -1
- package/data/remediations.json +1167 -0
- package/data/rules/ai-bom.yaml +78 -0
- package/data/rules/asar-archives.yaml +1 -1
- package/data/rules/build-fidelity.yaml +782 -0
- package/data/rules/cargo-rust.yaml +132 -0
- package/data/rules/mcp-servers.yaml +122 -0
- package/data/rules/package-integrity.yaml +13 -13
- package/lib/audit/index.js +48 -4
- package/lib/audit/scoring.js +2 -8
- package/lib/audit/targets.js +6 -1
- package/lib/cli/bomAssembly.js +32 -1
- package/lib/cli/cliOptions.js +94 -7
- package/lib/cli/index.js +268 -49
- package/lib/cli/jsBom.js +91 -5
- package/lib/cli/jvmBom.js +1515 -1314
- package/lib/cli/managedBom.js +249 -28
- package/lib/cli/nativeBom.js +986 -10
- package/lib/core/activity.js +120 -2
- package/lib/core/buildLedger.js +799 -0
- package/lib/core/deferredExit.js +141 -0
- package/lib/core/env.js +108 -2
- package/lib/core/fs.js +60 -0
- package/lib/core/logger.js +12 -99
- package/lib/core/severity.js +16 -0
- package/lib/core/syncFileWriter.js +106 -0
- package/lib/core/userAgent.js +151 -0
- package/lib/ecosystems/cargoCache.js +364 -0
- package/lib/ecosystems/caxa.js +13 -5
- package/lib/ecosystems/cppEvidence.js +40 -2
- package/lib/ecosystems/ecosystems.js +250 -91
- package/lib/ecosystems/golem.js +11 -1
- package/lib/ecosystems/gradleutils.js +498 -17
- package/lib/ecosystems/kosi.js +1256 -0
- package/lib/ecosystems/parsers-crystal.js +179 -0
- package/lib/ecosystems/parsers-dotnet.js +69 -39
- package/lib/ecosystems/parsers-elm.js +816 -0
- package/lib/ecosystems/parsers-gleam.js +1 -1
- package/lib/ecosystems/parsers-js.js +26 -0
- package/lib/ecosystems/parsers-julia.js +268 -0
- package/lib/ecosystems/parsers-jvm.js +9 -0
- package/lib/ecosystems/parsers-lua.js +218 -0
- package/lib/ecosystems/parsers-misc.js +188 -2
- package/lib/ecosystems/parsers-nim.js +221 -0
- package/lib/ecosystems/parsers-opam.js +180 -0
- package/lib/ecosystems/parsers-perl.js +277 -0
- package/lib/ecosystems/parsers-python.js +172 -36
- package/lib/ecosystems/parsers-r.js +148 -0
- package/lib/ecosystems/parsers-rebar.js +411 -0
- package/lib/ecosystems/parsers-rust.js +944 -19
- package/lib/ecosystems/parsers-spack.js +204 -0
- package/lib/ecosystems/parsers-stack.js +264 -0
- package/lib/ecosystems/parsers-terraform.js +194 -0
- package/lib/ecosystems/pypiClassifiers.js +326 -0
- package/lib/ecosystems/pythonutils.js +49 -1
- package/lib/ecosystems/rubyutils.js +13 -0
- package/lib/ecosystems/rusi.js +126 -6
- package/lib/evinser/evinser.js +141 -13
- package/lib/helpers/agenticToolutils.js +925 -0
- package/lib/helpers/remote/dependency-track.js +81 -14
- package/lib/inventory/agentFormulationParser.js +57 -4
- package/lib/inventory/aiCollector.js +504 -10
- package/lib/inventory/aiInventory.js +12 -0
- package/lib/inventory/analyzer.js +488 -30
- package/lib/inventory/astWorker.js +6 -0
- package/lib/inventory/auditCategories.js +17 -0
- package/lib/inventory/cbomutils.js +30 -0
- package/lib/inventory/cdxrs.js +7 -2
- package/lib/inventory/communityAiConfigParser.js +215 -2
- package/lib/inventory/depsUtils.js +72 -0
- package/lib/inventory/dosai.js +508 -22
- package/lib/inventory/envcontext.js +215 -24
- package/lib/inventory/evidenceUtils.js +13 -0
- package/lib/inventory/fetchBatch.js +131 -2
- package/lib/inventory/fetchRate.js +74 -11
- package/lib/inventory/forgeEnricher.js +3 -2
- package/lib/inventory/jvmToolEnv.js +594 -0
- package/lib/inventory/mcpConfigParser.js +166 -2
- package/lib/inventory/mcpDiscovery.js +11 -0
- package/lib/inventory/mcpServerManifest.js +389 -0
- package/lib/inventory/mcpbBundle.js +419 -0
- package/lib/inventory/osinfo.js +1 -0
- package/lib/inventory/plugins.js +3 -0
- package/lib/inventory/protobom.js +54 -88
- package/lib/inventory/protobomLoader.js +4 -0
- package/lib/inventory/rustFormulationParser.js +1 -1
- package/lib/inventory/source.js +10 -4
- package/lib/inventory/stylePackageRefs.js +41 -0
- package/lib/inventory/svelteUtils.js +524 -0
- package/lib/inventory/toolRequirements.js +903 -0
- package/lib/managers/binary.js +24 -0
- package/lib/managers/docker.js +26 -5
- package/lib/parsers/huggingfaceUtils.js +15 -7
- package/lib/parsers/onnx.js +294 -0
- package/lib/parsers/safetensors.js +114 -0
- package/lib/server/server.js +1 -1
- package/lib/stages/postgen/annotationBuilder.js +97 -0
- package/lib/stages/postgen/auditBom.js +76 -79
- package/lib/stages/postgen/introspection/annotate.js +320 -0
- package/lib/stages/postgen/introspection/commandFacts.js +293 -0
- package/lib/stages/postgen/introspection/emit.js +192 -0
- package/lib/stages/postgen/introspection/formulationEvidence.js +427 -0
- package/lib/stages/postgen/introspection/markers.js +250 -0
- package/lib/stages/postgen/introspection/reflect.js +1214 -0
- package/lib/stages/postgen/introspection/report.js +1208 -0
- package/lib/stages/postgen/introspection/score.js +1067 -0
- package/lib/stages/postgen/introspection/shapeCommand.js +304 -0
- package/lib/stages/postgen/parentEdge.js +156 -0
- package/lib/stages/postgen/postgen.js +284 -3
- package/lib/stages/postgen/ruleEngine.js +17 -0
- package/lib/stages/pregen/envAudit.js +42 -2
- package/lib/stages/pregen/pregen.js +1171 -6
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
- package/lib/validator/bomValidator.js +11 -2
- package/package.json +26 -26
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/scoring.d.ts +2 -8
- package/types/lib/audit/scoring.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +2 -1
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +10 -2
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +23 -0
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +33 -0
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +88 -8
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/buildLedger.d.ts +272 -0
- package/types/lib/core/buildLedger.d.ts.map +1 -0
- package/types/lib/core/deferredExit.d.ts +84 -0
- package/types/lib/core/deferredExit.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +43 -1
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +21 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/logger.d.ts +2 -11
- package/types/lib/core/logger.d.ts.map +1 -1
- package/types/lib/core/severity.d.ts +11 -0
- package/types/lib/core/severity.d.ts.map +1 -0
- package/types/lib/core/syncFileWriter.d.ts +33 -0
- package/types/lib/core/syncFileWriter.d.ts.map +1 -0
- package/types/lib/core/userAgent.d.ts +56 -0
- package/types/lib/core/userAgent.d.ts.map +1 -0
- package/types/lib/ecosystems/cargoCache.d.ts +75 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +15 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +53 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts +73 -0
- package/types/lib/ecosystems/kosi.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
- package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
- package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-misc.d.ts +23 -0
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
- package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
- package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
- package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-r.d.ts +35 -0
- package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
- package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
- package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
- package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts +80 -0
- package/types/lib/ecosystems/pypiClassifiers.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +21 -1
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +34 -1
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +87 -0
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +45 -4
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +12 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/depsUtils.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +43 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +43 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchRate.d.ts +9 -6
- package/types/lib/inventory/fetchRate.d.ts.map +1 -1
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +190 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
- package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
- package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
- package/types/lib/inventory/mcpbBundle.d.ts +19 -0
- package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/plugins.d.ts.map +1 -1
- package/types/lib/inventory/protobom.d.ts +18 -13
- package/types/lib/inventory/protobom.d.ts.map +1 -1
- package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
- package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
- package/types/lib/inventory/svelteUtils.d.ts +124 -0
- package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
- package/types/lib/inventory/toolRequirements.d.ts +216 -0
- package/types/lib/inventory/toolRequirements.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/onnx.d.ts +28 -0
- package/types/lib/parsers/onnx.d.ts.map +1 -0
- package/types/lib/parsers/safetensors.d.ts +22 -0
- package/types/lib/parsers/safetensors.d.ts.map +1 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts +53 -0
- package/types/lib/stages/postgen/annotationBuilder.d.ts.map +1 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/annotate.d.ts +81 -0
- package/types/lib/stages/postgen/introspection/annotate.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts +32 -0
- package/types/lib/stages/postgen/introspection/commandFacts.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts +71 -0
- package/types/lib/stages/postgen/introspection/emit.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts +154 -0
- package/types/lib/stages/postgen/introspection/formulationEvidence.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts +58 -0
- package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts +77 -0
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/report.d.ts +85 -0
- package/types/lib/stages/postgen/introspection/report.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/score.d.ts +149 -0
- package/types/lib/stages/postgen/introspection/score.d.ts.map +1 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts +79 -0
- package/types/lib/stages/postgen/introspection/shapeCommand.d.ts.map +1 -0
- package/types/lib/stages/postgen/parentEdge.d.ts +35 -0
- package/types/lib/stages/postgen/parentEdge.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +35 -2
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +131 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
import semver from "semver";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
cdxgenAgent,
|
|
10
|
+
DEBUG_MODE,
|
|
11
|
+
readEnvironmentVariable,
|
|
12
|
+
} from "../core/activity.js";
|
|
13
|
+
import { safeExistsSync } from "../core/fs.js";
|
|
14
|
+
import {
|
|
15
|
+
prefetchEnabled,
|
|
16
|
+
prefetchedResponse,
|
|
17
|
+
prefetchJson,
|
|
18
|
+
} from "../inventory/fetchBatch.js";
|
|
19
|
+
import { tryBuildPurl } from "../inventory/purl.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Elm project parser.
|
|
23
|
+
*
|
|
24
|
+
* Three files are consulted, spanning the two manifest generations of the
|
|
25
|
+
* ecosystem:
|
|
26
|
+
*
|
|
27
|
+
* - `elm.json` (0.19) — `type: "application"` projects pin **exact**
|
|
28
|
+
* versions for direct, indirect, and test dependencies, so the file
|
|
29
|
+
* doubles as the lock file. `type: "package"` projects declare bounded
|
|
30
|
+
* ranges (`"1.0.0 <= v < 2.0.0"`) and carry their own name, version,
|
|
31
|
+
* license, and summary.
|
|
32
|
+
* - `elm-package.json` (0.18) — the legacy manifest, declaring ranges only.
|
|
33
|
+
* - `elm-stuff/exact-dependencies.json` (0.18) — the lock file the old
|
|
34
|
+
* `elm-package` tool wrote with the exact version of every package.
|
|
35
|
+
*
|
|
36
|
+
* Package metadata (license, summary, and the dependencies of a dependency)
|
|
37
|
+
* is enriched from the local Elm cache when it is populated. The cache layout
|
|
38
|
+
* follows the compiler's own resolution (see `builder/src/Stuff.hs` in
|
|
39
|
+
* elm/compiler): `$ELM_HOME` (defaulting to `~/.elm`, or `%APPDATA%\elm` on
|
|
40
|
+
* Windows) contains one directory per compiler version, and each downloaded
|
|
41
|
+
* package lives at `<version>/packages/<author>/<project>/<version>/elm.json`.
|
|
42
|
+
* Parsing itself never reaches the network; {@link getElmMetadata} fills the
|
|
43
|
+
* gaps an empty cache leaves from the registry, and only when the run has
|
|
44
|
+
* asked for registry metadata.
|
|
45
|
+
*
|
|
46
|
+
* No `elm` purl type is registered in purl-spec, so packages are identified
|
|
47
|
+
* as generic packages carrying a `cdx:purl:proposedType=elm` property,
|
|
48
|
+
* following the convention used for crystal and nim. The `author/project`
|
|
49
|
+
* name maps to the purl namespace and the CycloneDX group so the full
|
|
50
|
+
* identity survives.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* One path segment of an Elm package name (`author` or `project`). Names are
|
|
55
|
+
* GitHub `user/repo` pairs; the shape check doubles as path-traversal
|
|
56
|
+
* protection because segments are joined into cache paths.
|
|
57
|
+
*/
|
|
58
|
+
const ELM_NAME_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The bounded-interval constraint syntax Elm uses in manifests:
|
|
62
|
+
* `"<lower> <= v < <upper>"`.
|
|
63
|
+
*/
|
|
64
|
+
const ELM_RANGE_PATTERN = /^(\S+)\s*(<=|<)\s*v\s*(<=|<)\s*(\S+)$/;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse an Elm project from its 0.19 `elm.json`.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} elmJsonFile Path to `elm.json`
|
|
70
|
+
* @param {object} [cacheReader] Reader returned by {@link createElmCacheReader};
|
|
71
|
+
* when omitted, one is derived from the manifest's pinned compiler version
|
|
72
|
+
* @returns {{ pkgList: object[], dependencies: object[], parentComponent: object, rootInputs: string[] }}
|
|
73
|
+
*/
|
|
74
|
+
export function parseElmProject(elmJsonFile, cacheReader) {
|
|
75
|
+
const manifest = readJsonFile(elmJsonFile);
|
|
76
|
+
if (
|
|
77
|
+
!manifest ||
|
|
78
|
+
(manifest.type !== "application" && manifest.type !== "package")
|
|
79
|
+
) {
|
|
80
|
+
return {
|
|
81
|
+
pkgList: [],
|
|
82
|
+
dependencies: [],
|
|
83
|
+
parentComponent: {},
|
|
84
|
+
rootInputs: [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const elmVersion =
|
|
89
|
+
typeof manifest["elm-version"] === "string"
|
|
90
|
+
? manifest["elm-version"]
|
|
91
|
+
: undefined;
|
|
92
|
+
const cache =
|
|
93
|
+
cacheReader ||
|
|
94
|
+
createElmCacheReader(
|
|
95
|
+
manifest.type === "application" && semver.valid(elmVersion)
|
|
96
|
+
? elmVersion
|
|
97
|
+
: undefined,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const parentComponent = {};
|
|
101
|
+
const properties = [{ name: "internal:SrcFile", value: elmJsonFile }];
|
|
102
|
+
if (elmVersion) {
|
|
103
|
+
properties.push({ name: "cdx:elm:elmVersion", value: elmVersion });
|
|
104
|
+
}
|
|
105
|
+
if (manifest.type === "package") {
|
|
106
|
+
const split = splitElmName(manifest.name);
|
|
107
|
+
parentComponent.type = "application";
|
|
108
|
+
if (split) {
|
|
109
|
+
parentComponent.group = split.author;
|
|
110
|
+
parentComponent.name = split.repo;
|
|
111
|
+
} else {
|
|
112
|
+
parentComponent.name = projectDirectoryName(elmJsonFile);
|
|
113
|
+
}
|
|
114
|
+
if (typeof manifest.version === "string" && manifest.version) {
|
|
115
|
+
parentComponent.version = manifest.version;
|
|
116
|
+
}
|
|
117
|
+
if (typeof manifest.summary === "string" && manifest.summary) {
|
|
118
|
+
parentComponent.description = manifest.summary;
|
|
119
|
+
}
|
|
120
|
+
if (typeof manifest.license === "string" && manifest.license) {
|
|
121
|
+
parentComponent.license = manifest.license;
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// Applications carry no name of their own; the directory stands in.
|
|
125
|
+
parentComponent.type = "application";
|
|
126
|
+
parentComponent.name = projectDirectoryName(elmJsonFile);
|
|
127
|
+
}
|
|
128
|
+
parentComponent.properties = properties;
|
|
129
|
+
|
|
130
|
+
const entries = [];
|
|
131
|
+
if (manifest.type === "package") {
|
|
132
|
+
addRangeDependencies(
|
|
133
|
+
entries,
|
|
134
|
+
manifest.dependencies,
|
|
135
|
+
"direct",
|
|
136
|
+
elmJsonFile,
|
|
137
|
+
cache,
|
|
138
|
+
);
|
|
139
|
+
addRangeDependencies(
|
|
140
|
+
entries,
|
|
141
|
+
manifest["test-dependencies"],
|
|
142
|
+
"direct-test",
|
|
143
|
+
elmJsonFile,
|
|
144
|
+
cache,
|
|
145
|
+
);
|
|
146
|
+
} else {
|
|
147
|
+
for (const [section, kind] of [
|
|
148
|
+
["dependencies", "direct"],
|
|
149
|
+
["test-dependencies", "direct-test"],
|
|
150
|
+
]) {
|
|
151
|
+
const group = manifest[section];
|
|
152
|
+
addExactDependencies(entries, group?.direct, kind, elmJsonFile, cache);
|
|
153
|
+
addExactDependencies(
|
|
154
|
+
entries,
|
|
155
|
+
group?.indirect,
|
|
156
|
+
kind === "direct" ? "indirect" : "indirect-test",
|
|
157
|
+
elmJsonFile,
|
|
158
|
+
cache,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const pkgList = entries.map((entry) => entry.pkg);
|
|
163
|
+
const rootInputs = entries
|
|
164
|
+
.filter((entry) => entry.kind === "direct" || entry.kind === "direct-test")
|
|
165
|
+
.map((entry) => entry.pkg["bom-ref"]);
|
|
166
|
+
return {
|
|
167
|
+
pkgList,
|
|
168
|
+
dependencies: buildDependencyEdges(entries),
|
|
169
|
+
parentComponent,
|
|
170
|
+
rootInputs,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Parse a legacy 0.18 Elm project from `elm-package.json` and its optional
|
|
176
|
+
* `elm-stuff/exact-dependencies.json` lock file.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} elmPackageJsonFile Path to `elm-package.json`
|
|
179
|
+
* @param {string} [exactDepsFile] Path to the lock file, if present
|
|
180
|
+
* @returns {{ pkgList: object[], dependencies: object[], parentComponent: object, rootInputs: string[] }}
|
|
181
|
+
*/
|
|
182
|
+
export function parseLegacyElmProject(elmPackageJsonFile, exactDepsFile) {
|
|
183
|
+
const manifest = readJsonFile(elmPackageJsonFile);
|
|
184
|
+
if (
|
|
185
|
+
!manifest ||
|
|
186
|
+
typeof manifest !== "object" ||
|
|
187
|
+
(!manifest["elm-version"] && !manifest.dependencies)
|
|
188
|
+
) {
|
|
189
|
+
return {
|
|
190
|
+
pkgList: [],
|
|
191
|
+
dependencies: [],
|
|
192
|
+
parentComponent: {},
|
|
193
|
+
rootInputs: [],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const parentComponent = {
|
|
198
|
+
type: "application",
|
|
199
|
+
name: projectDirectoryName(elmPackageJsonFile),
|
|
200
|
+
};
|
|
201
|
+
if (typeof manifest.version === "string" && manifest.version) {
|
|
202
|
+
parentComponent.version = manifest.version;
|
|
203
|
+
}
|
|
204
|
+
if (typeof manifest.summary === "string" && manifest.summary) {
|
|
205
|
+
parentComponent.description = manifest.summary;
|
|
206
|
+
}
|
|
207
|
+
if (typeof manifest.license === "string" && manifest.license) {
|
|
208
|
+
parentComponent.license = manifest.license;
|
|
209
|
+
}
|
|
210
|
+
const properties = [{ name: "internal:SrcFile", value: elmPackageJsonFile }];
|
|
211
|
+
if (typeof manifest["elm-version"] === "string" && manifest["elm-version"]) {
|
|
212
|
+
properties.push({
|
|
213
|
+
name: "cdx:elm:elmVersion",
|
|
214
|
+
value: manifest["elm-version"],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
parentComponent.properties = properties;
|
|
218
|
+
|
|
219
|
+
const declared = new Set(
|
|
220
|
+
manifest.dependencies && typeof manifest.dependencies === "object"
|
|
221
|
+
? Object.keys(manifest.dependencies)
|
|
222
|
+
: [],
|
|
223
|
+
);
|
|
224
|
+
const ranges =
|
|
225
|
+
manifest.dependencies && typeof manifest.dependencies === "object"
|
|
226
|
+
? manifest.dependencies
|
|
227
|
+
: {};
|
|
228
|
+
|
|
229
|
+
const entries = [];
|
|
230
|
+
const exact = exactDepsFile ? readJsonFile(exactDepsFile) : undefined;
|
|
231
|
+
const pinned =
|
|
232
|
+
exact && typeof exact === "object" && !Array.isArray(exact) ? exact : {};
|
|
233
|
+
for (const [name, version] of Object.entries(pinned)) {
|
|
234
|
+
const kind = declared.has(name) ? "direct" : "indirect";
|
|
235
|
+
entries.push({
|
|
236
|
+
kind,
|
|
237
|
+
name,
|
|
238
|
+
pkg: elmPackage(name, typeof version === "string" ? version : undefined, {
|
|
239
|
+
dependency: kind,
|
|
240
|
+
srcFile: exactDepsFile,
|
|
241
|
+
versionRange:
|
|
242
|
+
typeof ranges[name] === "string" ? ranges[name] : undefined,
|
|
243
|
+
}),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (!entries.length) {
|
|
247
|
+
// Lockless fallback: the declared dependencies without resolved versions.
|
|
248
|
+
for (const [name, range] of Object.entries(ranges)) {
|
|
249
|
+
entries.push({
|
|
250
|
+
kind: "direct",
|
|
251
|
+
name,
|
|
252
|
+
pkg: elmPackage(name, undefined, {
|
|
253
|
+
dependency: "direct",
|
|
254
|
+
srcFile: elmPackageJsonFile,
|
|
255
|
+
versionRange: typeof range === "string" ? range : undefined,
|
|
256
|
+
}),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const rootInputs = entries
|
|
261
|
+
.filter((entry) => entry.kind === "direct")
|
|
262
|
+
.map((entry) => entry.pkg["bom-ref"]);
|
|
263
|
+
return {
|
|
264
|
+
pkgList: entries.map((entry) => entry.pkg),
|
|
265
|
+
dependencies: buildDependencyEdges(entries),
|
|
266
|
+
parentComponent,
|
|
267
|
+
rootInputs,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Tell whether a candidate `elm.json` really is an Elm manifest. The file
|
|
273
|
+
* name is distinctive but lives in every Elm project, so detection only
|
|
274
|
+
* needs a cheap content check before dispatching.
|
|
275
|
+
*
|
|
276
|
+
* @param {string} filePath Path to the candidate `elm.json`
|
|
277
|
+
* @returns {boolean} true when the file declares an Elm project
|
|
278
|
+
*/
|
|
279
|
+
export function isElmProjectFile(filePath) {
|
|
280
|
+
const manifest = readJsonFile(filePath);
|
|
281
|
+
return manifest?.type === "application" || manifest?.type === "package";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Create a reader for the local Elm package cache. Reads are memoised and
|
|
286
|
+
* silently disabled when the cache directory does not exist.
|
|
287
|
+
*
|
|
288
|
+
* @param {string} [elmVersionHint] Exact compiler version directory to try
|
|
289
|
+
* first, when the project pins one
|
|
290
|
+
* @returns {{ readMetadata: (name: string, version: string) => object|null, listVersions: (name: string) => string[] }}
|
|
291
|
+
*/
|
|
292
|
+
export function createElmCacheReader(elmVersionHint) {
|
|
293
|
+
const home = elmHomeDir();
|
|
294
|
+
const metadataCache = new Map();
|
|
295
|
+
const versionsCache = new Map();
|
|
296
|
+
let versionDirs;
|
|
297
|
+
|
|
298
|
+
function listVersionDirs() {
|
|
299
|
+
if (versionDirs) {
|
|
300
|
+
return versionDirs;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
versionDirs = readdirSync(home)
|
|
304
|
+
.filter((entry) => semver.valid(entry))
|
|
305
|
+
.sort()
|
|
306
|
+
.reverse();
|
|
307
|
+
} catch {
|
|
308
|
+
versionDirs = [];
|
|
309
|
+
}
|
|
310
|
+
return versionDirs;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function packageDir(name) {
|
|
314
|
+
const split = splitElmName(name);
|
|
315
|
+
return split
|
|
316
|
+
? { author: split.author, repo: split.repo, valid: true }
|
|
317
|
+
: { valid: false };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
readMetadata(name, version) {
|
|
322
|
+
const key = `${name}@${version}`;
|
|
323
|
+
if (metadataCache.has(key)) {
|
|
324
|
+
return metadataCache.get(key);
|
|
325
|
+
}
|
|
326
|
+
let metadata = null;
|
|
327
|
+
const dir = packageDir(name);
|
|
328
|
+
if (dir.valid && semver.valid(version)) {
|
|
329
|
+
const dirs =
|
|
330
|
+
elmVersionHint && semver.valid(elmVersionHint)
|
|
331
|
+
? [
|
|
332
|
+
elmVersionHint,
|
|
333
|
+
...listVersionDirs().filter((d) => d !== elmVersionHint),
|
|
334
|
+
]
|
|
335
|
+
: listVersionDirs();
|
|
336
|
+
for (const versionDir of dirs) {
|
|
337
|
+
const metaFile = join(
|
|
338
|
+
home,
|
|
339
|
+
versionDir,
|
|
340
|
+
"packages",
|
|
341
|
+
dir.author,
|
|
342
|
+
dir.repo,
|
|
343
|
+
version,
|
|
344
|
+
"elm.json",
|
|
345
|
+
);
|
|
346
|
+
if (safeExistsSync(metaFile)) {
|
|
347
|
+
metadata = readJsonFile(metaFile);
|
|
348
|
+
if (metadata) {
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
metadataCache.set(key, metadata);
|
|
355
|
+
return metadata;
|
|
356
|
+
},
|
|
357
|
+
listVersions(name) {
|
|
358
|
+
if (versionsCache.has(name)) {
|
|
359
|
+
return versionsCache.get(name);
|
|
360
|
+
}
|
|
361
|
+
const versions = new Set();
|
|
362
|
+
const dir = packageDir(name);
|
|
363
|
+
if (dir.valid) {
|
|
364
|
+
for (const versionDir of listVersionDirs()) {
|
|
365
|
+
try {
|
|
366
|
+
for (const entry of readdirSync(
|
|
367
|
+
join(home, versionDir, "packages", dir.author, dir.repo),
|
|
368
|
+
)) {
|
|
369
|
+
if (semver.valid(entry)) {
|
|
370
|
+
versions.add(entry);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} catch {
|
|
374
|
+
// No cached copy under this compiler version.
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const list = [...versions];
|
|
379
|
+
versionsCache.set(name, list);
|
|
380
|
+
return list;
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Resolve the Elm home directory the way the compiler does: `$ELM_HOME` when
|
|
387
|
+
* set, otherwise the OS per-user data directory for "elm" (`~/.elm` on Unix,
|
|
388
|
+
* `%APPDATA%\elm` on Windows).
|
|
389
|
+
*
|
|
390
|
+
* @returns {string} Elm home directory
|
|
391
|
+
*/
|
|
392
|
+
function elmHomeDir() {
|
|
393
|
+
const custom = readEnvironmentVariable("ELM_HOME");
|
|
394
|
+
if (custom) {
|
|
395
|
+
return custom;
|
|
396
|
+
}
|
|
397
|
+
if (process.platform === "win32") {
|
|
398
|
+
const appData =
|
|
399
|
+
readEnvironmentVariable("APPDATA") ||
|
|
400
|
+
join(homedir(), "AppData", "Roaming");
|
|
401
|
+
return join(appData, "elm");
|
|
402
|
+
}
|
|
403
|
+
return join(homedir(), ".elm");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Split an Elm package name into its author and project segments.
|
|
408
|
+
*
|
|
409
|
+
* @param {string} name Package name, e.g. `elm/http`
|
|
410
|
+
* @returns {{author: string, repo: string}|null} null when the name is not a
|
|
411
|
+
* well-formed `author/project` pair
|
|
412
|
+
*/
|
|
413
|
+
function splitElmName(name) {
|
|
414
|
+
if (typeof name !== "string") {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
const parts = name.split("/");
|
|
418
|
+
if (parts.length !== 2 || !parts.every((p) => ELM_NAME_SEGMENT.test(p))) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
return { author: parts[0], repo: parts[1] };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Derive a project display name from the manifest's directory, used by
|
|
426
|
+
* application manifests, which carry no name of their own.
|
|
427
|
+
*
|
|
428
|
+
* @param {string} manifestFile Manifest path
|
|
429
|
+
* @returns {string} Directory base name
|
|
430
|
+
*/
|
|
431
|
+
function projectDirectoryName(manifestFile) {
|
|
432
|
+
return basename(dirname(resolve(manifestFile)));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Translate an Elm version constraint into semver range syntax. Only the
|
|
437
|
+
* documented bounded interval (`"1.0.0 <= v < 2.0.0"`) is understood;
|
|
438
|
+
* anything else is returned as null and treated as an opaque constraint.
|
|
439
|
+
*
|
|
440
|
+
* @param {string} range Constraint string from a manifest
|
|
441
|
+
* @returns {string|null} Equivalent semver range, e.g. `">=1.0.0 <2.0.0"`
|
|
442
|
+
*/
|
|
443
|
+
function toSemverRange(range) {
|
|
444
|
+
if (typeof range !== "string") {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
const match = range.match(ELM_RANGE_PATTERN);
|
|
448
|
+
if (!match) {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
const [, lower, lowerOp, upperOp, upper] = match;
|
|
452
|
+
// The grammar allows `<= v` lower bounds only; a `< v` lower bound would
|
|
453
|
+
// exclude the very version the resolver needs, so treat it as opaque.
|
|
454
|
+
if (lowerOp !== "<=" || !semver.valid(lower) || !semver.valid(upper)) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
return `>=${lower} ${upperOp}${upper}`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Append the dependencies of one exact-version section of an application
|
|
462
|
+
* `elm.json`, enriching each from the local cache when possible.
|
|
463
|
+
*
|
|
464
|
+
* @param {object[]} entries Accumulator of `{kind, name, pkg, metadata}` records
|
|
465
|
+
*/
|
|
466
|
+
function addExactDependencies(entries, section, kind, srcFile, cache) {
|
|
467
|
+
if (!section || typeof section !== "object") {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
for (const [name, version] of Object.entries(section)) {
|
|
471
|
+
const exact = typeof version === "string" && version ? version : undefined;
|
|
472
|
+
const metadata =
|
|
473
|
+
cache && exact && semver.valid(exact) && splitElmName(name)
|
|
474
|
+
? cache.readMetadata(name, exact)
|
|
475
|
+
: null;
|
|
476
|
+
entries.push({
|
|
477
|
+
kind,
|
|
478
|
+
name,
|
|
479
|
+
metadata,
|
|
480
|
+
pkg: elmPackage(name, exact, {
|
|
481
|
+
dependency: kind,
|
|
482
|
+
srcFile,
|
|
483
|
+
metadata,
|
|
484
|
+
}),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Append the dependencies of a range-constrained section of a package
|
|
491
|
+
* `elm.json`. When the local Elm cache holds versions satisfying the declared
|
|
492
|
+
* range, the newest of them — the one Elm's solver would pick — becomes the
|
|
493
|
+
* component version; otherwise the component keeps only the constraint.
|
|
494
|
+
*
|
|
495
|
+
* @param {object[]} entries Accumulator of `{kind, name, pkg, metadata}` records
|
|
496
|
+
*/
|
|
497
|
+
function addRangeDependencies(entries, section, kind, srcFile, cache) {
|
|
498
|
+
if (!section || typeof section !== "object") {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
for (const [name, range] of Object.entries(section)) {
|
|
502
|
+
let version;
|
|
503
|
+
let metadata;
|
|
504
|
+
const semverRange = toSemverRange(range);
|
|
505
|
+
if (cache && semverRange && splitElmName(name)) {
|
|
506
|
+
// Elm's solver picks the newest version within the constraints.
|
|
507
|
+
version =
|
|
508
|
+
semver.maxSatisfying(cache.listVersions(name), semverRange) ||
|
|
509
|
+
undefined;
|
|
510
|
+
if (version) {
|
|
511
|
+
metadata = cache.readMetadata(name, version);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
entries.push({
|
|
515
|
+
kind,
|
|
516
|
+
name,
|
|
517
|
+
metadata,
|
|
518
|
+
pkg: elmPackage(name, version, {
|
|
519
|
+
dependency: kind,
|
|
520
|
+
srcFile,
|
|
521
|
+
metadata,
|
|
522
|
+
versionRange: typeof range === "string" ? range : undefined,
|
|
523
|
+
}),
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Build the dependency graph for the packages of a single manifest.
|
|
530
|
+
*
|
|
531
|
+
* Elm manifests list the resolved set but not the edges between its members;
|
|
532
|
+
* those live in each package's own cached `elm.json`, which the enrichment
|
|
533
|
+
* pass has already read. Edges are therefore emitted only for packages found
|
|
534
|
+
* in the local cache — every component still gets an entry, so none of them
|
|
535
|
+
* is left dangling outside the graph.
|
|
536
|
+
*
|
|
537
|
+
* @param {object[]} entries `{kind, name, pkg, metadata}` records of one manifest
|
|
538
|
+
* @returns {object[]} CycloneDX dependency entries
|
|
539
|
+
*/
|
|
540
|
+
function buildDependencyEdges(entries) {
|
|
541
|
+
const refByName = new Map(
|
|
542
|
+
entries.map((entry) => [entry.name, entry.pkg["bom-ref"]]),
|
|
543
|
+
);
|
|
544
|
+
return entries.map((entry) => {
|
|
545
|
+
const declared = entry.metadata?.dependencies;
|
|
546
|
+
const dependsOn =
|
|
547
|
+
declared && typeof declared === "object" && !Array.isArray(declared)
|
|
548
|
+
? Object.keys(declared)
|
|
549
|
+
.map((name) => refByName.get(name))
|
|
550
|
+
.filter((ref) => ref && ref !== entry.pkg["bom-ref"])
|
|
551
|
+
: [];
|
|
552
|
+
return {
|
|
553
|
+
ref: entry.pkg["bom-ref"],
|
|
554
|
+
dependsOn: [...new Set(dependsOn)].sort(),
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Build a component-like package record for an Elm dependency.
|
|
561
|
+
*
|
|
562
|
+
* @param {string} name Package name as Elm writes it (`author/project`)
|
|
563
|
+
* @param {string|undefined} version Resolved version, if known
|
|
564
|
+
* @param {object} opts Extra context (`dependency`, `srcFile`, `metadata`, `versionRange`)
|
|
565
|
+
* @returns {object} Package record
|
|
566
|
+
*/
|
|
567
|
+
function elmPackage(name, version, opts) {
|
|
568
|
+
const split = splitElmName(name);
|
|
569
|
+
const test =
|
|
570
|
+
opts.dependency === "direct-test" || opts.dependency === "indirect-test";
|
|
571
|
+
const purl = tryBuildPurl({
|
|
572
|
+
type: "generic",
|
|
573
|
+
namespace: split?.author,
|
|
574
|
+
name: split?.repo ?? name,
|
|
575
|
+
version: version || undefined,
|
|
576
|
+
});
|
|
577
|
+
const properties = [
|
|
578
|
+
{ name: "internal:SrcFile", value: opts.srcFile },
|
|
579
|
+
{ name: "cdx:purl:proposedType", value: "elm" },
|
|
580
|
+
{ name: "cdx:elm:dependency", value: opts.dependency },
|
|
581
|
+
];
|
|
582
|
+
if (opts.versionRange) {
|
|
583
|
+
properties.push({
|
|
584
|
+
name: "cdx:elm:versionRange",
|
|
585
|
+
value: opts.versionRange,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
const pkg = {
|
|
589
|
+
name: split?.repo ?? name,
|
|
590
|
+
group: split?.author,
|
|
591
|
+
...(version ? { version } : {}),
|
|
592
|
+
type: "library",
|
|
593
|
+
scope: test ? "optional" : "required",
|
|
594
|
+
properties,
|
|
595
|
+
};
|
|
596
|
+
if (split) {
|
|
597
|
+
// Both locations follow from the name alone: the registry only publishes
|
|
598
|
+
// packages hosted on GitHub under the same `author/project` path, and
|
|
599
|
+
// documents every version at a stable URL. The link is to the canonical
|
|
600
|
+
// site rather than to whatever `ELM_PACKAGE_URL` points at, since a
|
|
601
|
+
// mirror is where cdxgen read the metadata, not where a reader of the BOM
|
|
602
|
+
// should be sent.
|
|
603
|
+
pkg.repository = {
|
|
604
|
+
url: `https://github.com/${split.author}/${split.repo}`,
|
|
605
|
+
};
|
|
606
|
+
pkg.homepage = {
|
|
607
|
+
url: version
|
|
608
|
+
? `${ELM_REGISTRY_HOME}/packages/${split.author}/${split.repo}/${version}/`
|
|
609
|
+
: `${ELM_REGISTRY_HOME}/packages/${split.author}/${split.repo}/`,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
if (typeof opts.metadata?.license === "string" && opts.metadata.license) {
|
|
613
|
+
pkg.license = opts.metadata.license;
|
|
614
|
+
}
|
|
615
|
+
if (typeof opts.metadata?.summary === "string" && opts.metadata.summary) {
|
|
616
|
+
pkg.description = opts.metadata.summary;
|
|
617
|
+
}
|
|
618
|
+
if (purl) {
|
|
619
|
+
pkg.purl = purl;
|
|
620
|
+
pkg["bom-ref"] = decodeURIComponent(purl);
|
|
621
|
+
} else {
|
|
622
|
+
pkg["bom-ref"] = `library:${name}:${version || ""}`;
|
|
623
|
+
}
|
|
624
|
+
return pkg;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** The canonical package site, used for the documentation link of a package. */
|
|
628
|
+
const ELM_REGISTRY_HOME = "https://package.elm-lang.org";
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Base URL the registry documents are read from. `ELM_PACKAGE_URL` redirects
|
|
632
|
+
* lookups at a mirror, matching the `PUB_DEV_URL`/`RUST_CRATES_URL` convention.
|
|
633
|
+
*
|
|
634
|
+
* @returns {string} Registry base URL without a trailing slash
|
|
635
|
+
*/
|
|
636
|
+
function elmRegistryUrl() {
|
|
637
|
+
const custom = readEnvironmentVariable("ELM_PACKAGE_URL");
|
|
638
|
+
return (custom || ELM_REGISTRY_HOME).replace(/\/+$/, "");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Package path prefixes the registry's own `robots.txt` disallows for every
|
|
643
|
+
* user-agent. They are the pre-0.19 packages, which the site keeps serving but
|
|
644
|
+
* asks clients not to walk; a 0.18 project's dependencies fall entirely inside
|
|
645
|
+
* this set, so its metadata comes from the local cache or not at all.
|
|
646
|
+
*/
|
|
647
|
+
const ROBOTS_DISALLOWED_PREFIXES = [
|
|
648
|
+
"elm-lang/",
|
|
649
|
+
"elm-tools/",
|
|
650
|
+
"elm-community/elm-test/",
|
|
651
|
+
"evancz/elm-html/",
|
|
652
|
+
"evancz/elm-http/",
|
|
653
|
+
"evancz/elm-svg/",
|
|
654
|
+
"evancz/url-parser/",
|
|
655
|
+
"evancz/virtual-dom/",
|
|
656
|
+
];
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Whether the registry's `robots.txt` permits fetching a package's documents.
|
|
660
|
+
*
|
|
661
|
+
* @param {string} name Package name, `author/project`
|
|
662
|
+
* @returns {boolean} true when no `Disallow` rule covers the package
|
|
663
|
+
*/
|
|
664
|
+
export function isRegistryCrawlable(name) {
|
|
665
|
+
const path = `${name}/`;
|
|
666
|
+
return !ROBOTS_DISALLOWED_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Enrich Elm components from the package registry.
|
|
671
|
+
*
|
|
672
|
+
* Only the gaps left by the local cache are fetched, and only for packages
|
|
673
|
+
* with a resolved version that `robots.txt` permits. Two documents are read
|
|
674
|
+
* per package, both of which the compiler itself reads when it installs one:
|
|
675
|
+
*
|
|
676
|
+
* - `elm.json` — the published manifest, carrying the summary, the license
|
|
677
|
+
* and the package's own dependencies, which close the gaps in the
|
|
678
|
+
* dependency graph that an unpopulated cache leaves behind.
|
|
679
|
+
* - `endpoint.json` — the source archive URL and the SHA-1 of its bytes,
|
|
680
|
+
* which the compiler verifies after downloading. It becomes the
|
|
681
|
+
* component's distribution reference and hash.
|
|
682
|
+
*
|
|
683
|
+
* Requests carry cdxgen's contact user-agent and pass through the shared
|
|
684
|
+
* per-host rate policy for `package.elm-lang.org` (see `fetchRate.js`).
|
|
685
|
+
*
|
|
686
|
+
* @param {object[]} pkgList Components to enrich, mutated in place
|
|
687
|
+
* @param {object[]} [dependencies] Dependency edges to complete in place
|
|
688
|
+
* @returns {Promise<object[]>} The same package list
|
|
689
|
+
*/
|
|
690
|
+
export async function getElmMetadata(pkgList, dependencies) {
|
|
691
|
+
const registry = elmRegistryUrl();
|
|
692
|
+
const targets = pkgList.filter(
|
|
693
|
+
(pkg) =>
|
|
694
|
+
pkg.group &&
|
|
695
|
+
pkg.version &&
|
|
696
|
+
semver.valid(pkg.version) &&
|
|
697
|
+
splitElmName(`${pkg.group}/${pkg.name}`) &&
|
|
698
|
+
isRegistryCrawlable(`${pkg.group}/${pkg.name}`),
|
|
699
|
+
);
|
|
700
|
+
if (!targets.length) {
|
|
701
|
+
return pkgList;
|
|
702
|
+
}
|
|
703
|
+
const manifestUrl = (pkg) =>
|
|
704
|
+
`${registry}/packages/${pkg.group}/${pkg.name}/${pkg.version}/elm.json`;
|
|
705
|
+
const endpointUrl = (pkg) =>
|
|
706
|
+
`${registry}/packages/${pkg.group}/${pkg.name}/${pkg.version}/endpoint.json`;
|
|
707
|
+
const prefetched = await prefetchJson(
|
|
708
|
+
prefetchEnabled()
|
|
709
|
+
? targets.flatMap((pkg) => [
|
|
710
|
+
{ url: manifestUrl(pkg) },
|
|
711
|
+
{ url: endpointUrl(pkg) },
|
|
712
|
+
])
|
|
713
|
+
: [],
|
|
714
|
+
);
|
|
715
|
+
const manifests = new Map();
|
|
716
|
+
for (const pkg of targets) {
|
|
717
|
+
if (DEBUG_MODE) {
|
|
718
|
+
console.log(`Querying ${registry} for ${pkg.group}/${pkg.name}`);
|
|
719
|
+
}
|
|
720
|
+
const manifest = await fetchJson(manifestUrl(pkg), prefetched);
|
|
721
|
+
if (manifest) {
|
|
722
|
+
manifests.set(pkg["bom-ref"], manifest);
|
|
723
|
+
if (!pkg.license && typeof manifest.license === "string") {
|
|
724
|
+
pkg.license = manifest.license;
|
|
725
|
+
}
|
|
726
|
+
if (!pkg.description && typeof manifest.summary === "string") {
|
|
727
|
+
pkg.description = manifest.summary;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const endpoint = await fetchJson(endpointUrl(pkg), prefetched);
|
|
731
|
+
if (typeof endpoint?.url === "string") {
|
|
732
|
+
pkg.distribution = { url: endpoint.url };
|
|
733
|
+
}
|
|
734
|
+
if (/^[a-f0-9]{40}$/i.test(endpoint?.hash || "")) {
|
|
735
|
+
// The compiler streams the archive and compares this digest against the
|
|
736
|
+
// SHA-1 of the bytes it received, so it is a content hash of the
|
|
737
|
+
// distribution rather than a revision identifier.
|
|
738
|
+
pkg.hashes = [{ alg: "SHA-1", content: endpoint.hash.toLowerCase() }];
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (dependencies?.length && manifests.size) {
|
|
742
|
+
completeDependencyEdges(dependencies, pkgList, manifests);
|
|
743
|
+
}
|
|
744
|
+
return pkgList;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Fill in the edges of components the local cache could not describe, using
|
|
749
|
+
* the manifests fetched from the registry. Edges already resolved from the
|
|
750
|
+
* cache are left alone.
|
|
751
|
+
*
|
|
752
|
+
* @param {object[]} dependencies Dependency edges, mutated in place
|
|
753
|
+
* @param {object[]} pkgList Components the edges may point at
|
|
754
|
+
* @param {Map<string, object>} manifests Registry manifests by bom-ref
|
|
755
|
+
*/
|
|
756
|
+
function completeDependencyEdges(dependencies, pkgList, manifests) {
|
|
757
|
+
const refByName = new Map(
|
|
758
|
+
pkgList
|
|
759
|
+
.filter((pkg) => pkg.group)
|
|
760
|
+
.map((pkg) => [`${pkg.group}/${pkg.name}`, pkg["bom-ref"]]),
|
|
761
|
+
);
|
|
762
|
+
for (const edge of dependencies) {
|
|
763
|
+
if (edge.dependsOn?.length) {
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const declared = manifests.get(edge.ref)?.dependencies;
|
|
767
|
+
if (!declared || typeof declared !== "object" || Array.isArray(declared)) {
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
edge.dependsOn = [
|
|
771
|
+
...new Set(
|
|
772
|
+
Object.keys(declared)
|
|
773
|
+
.map((name) => refByName.get(name))
|
|
774
|
+
.filter((ref) => ref && ref !== edge.ref),
|
|
775
|
+
),
|
|
776
|
+
].sort();
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Read one registry document, preferring the batched prefetch and falling back
|
|
782
|
+
* to a direct request. A failure leaves the component as the manifests found
|
|
783
|
+
* it rather than aborting the enrichment.
|
|
784
|
+
*
|
|
785
|
+
* @param {string} url Document URL
|
|
786
|
+
* @param {Map} prefetched Batched results
|
|
787
|
+
* @returns {Promise<object|undefined>} Parsed document
|
|
788
|
+
*/
|
|
789
|
+
async function fetchJson(url, prefetched) {
|
|
790
|
+
try {
|
|
791
|
+
const res =
|
|
792
|
+
prefetchedResponse(prefetched, url) ||
|
|
793
|
+
(await cdxgenAgent.get(url, { responseType: "json" }));
|
|
794
|
+
return res?.body;
|
|
795
|
+
} catch (err) {
|
|
796
|
+
if (DEBUG_MODE) {
|
|
797
|
+
console.log(`Unable to fetch ${url}: ${err.message}`);
|
|
798
|
+
}
|
|
799
|
+
return undefined;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Read and parse a JSON file, warning instead of throwing on invalid input.
|
|
805
|
+
*
|
|
806
|
+
* @param {string} filePath File to read
|
|
807
|
+
* @returns {object|undefined} Parsed value
|
|
808
|
+
*/
|
|
809
|
+
function readJsonFile(filePath) {
|
|
810
|
+
try {
|
|
811
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
812
|
+
} catch (error) {
|
|
813
|
+
console.warn(`Failed to parse ${filePath}: ${error.message}`);
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
}
|