@trustify-da/trustify-da-javascript-client 0.3.0-ea.b8af0f8 → 0.3.0-ea.bbe2094

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.
Files changed (52) hide show
  1. package/README.md +73 -8
  2. package/dist/package.json +6 -5
  3. package/dist/src/analysis.js +3 -2
  4. package/dist/src/cli.js +51 -2
  5. package/dist/src/cyclone_dx_sbom.d.ts +14 -1
  6. package/dist/src/cyclone_dx_sbom.js +32 -5
  7. package/dist/src/index.d.ts +74 -3
  8. package/dist/src/index.js +89 -6
  9. package/dist/src/oci_image/utils.js +11 -2
  10. package/dist/src/provider.js +8 -0
  11. package/dist/src/providers/base_java.d.ts +0 -9
  12. package/dist/src/providers/base_java.js +2 -38
  13. package/dist/src/providers/base_javascript.d.ts +6 -0
  14. package/dist/src/providers/base_javascript.js +37 -6
  15. package/dist/src/providers/base_pyproject.d.ts +153 -0
  16. package/dist/src/providers/base_pyproject.js +315 -0
  17. package/dist/src/providers/golang_gomodules.d.ts +21 -12
  18. package/dist/src/providers/golang_gomodules.js +164 -118
  19. package/dist/src/providers/gomod_parser.d.ts +4 -0
  20. package/dist/src/providers/gomod_parser.js +16 -0
  21. package/dist/src/providers/java_gradle.d.ts +19 -0
  22. package/dist/src/providers/java_gradle.js +116 -2
  23. package/dist/src/providers/java_maven.d.ts +8 -0
  24. package/dist/src/providers/java_maven.js +93 -1
  25. package/dist/src/providers/javascript_bun.d.ts +10 -0
  26. package/dist/src/providers/javascript_bun.js +100 -0
  27. package/dist/src/providers/javascript_npm.d.ts +1 -0
  28. package/dist/src/providers/javascript_npm.js +21 -0
  29. package/dist/src/providers/javascript_pnpm.js +6 -2
  30. package/dist/src/providers/manifest.d.ts +2 -0
  31. package/dist/src/providers/manifest.js +22 -4
  32. package/dist/src/providers/marker_evaluator.d.ts +14 -0
  33. package/dist/src/providers/marker_evaluator.js +191 -0
  34. package/dist/src/providers/processors/yarn_berry_processor.js +88 -5
  35. package/dist/src/providers/python_controller.d.ts +5 -1
  36. package/dist/src/providers/python_controller.js +8 -4
  37. package/dist/src/providers/python_pip.d.ts +4 -0
  38. package/dist/src/providers/python_pip.js +5 -5
  39. package/dist/src/providers/python_pip_pyproject.d.ts +61 -0
  40. package/dist/src/providers/python_pip_pyproject.js +146 -0
  41. package/dist/src/providers/python_poetry.d.ts +75 -0
  42. package/dist/src/providers/python_poetry.js +238 -0
  43. package/dist/src/providers/python_uv.d.ts +55 -0
  44. package/dist/src/providers/python_uv.js +227 -0
  45. package/dist/src/providers/tree-sitter-gomod.wasm +0 -0
  46. package/dist/src/sbom.d.ts +14 -1
  47. package/dist/src/sbom.js +13 -2
  48. package/dist/src/tools.d.ts +26 -0
  49. package/dist/src/tools.js +58 -0
  50. package/dist/src/workspace.d.ts +9 -0
  51. package/dist/src/workspace.js +1 -1
  52. package/package.json +7 -6
package/dist/src/index.js CHANGED
@@ -6,14 +6,18 @@ import analysis from './analysis.js';
6
6
  import fs from 'node:fs';
7
7
  import { getCustom } from "./tools.js";
8
8
  import { resolveBatchMetadata, resolveContinueOnError } from './batch_opts.js';
9
+ import { discoverMavenModules } from './providers/java_maven.js';
10
+ import { discoverGradleSubprojects } from './providers/java_gradle.js';
11
+ import { discoverGoWorkspaceModules } from './providers/golang_gomodules.js';
12
+ import { discoverUvWorkspaceMembers } from './providers/python_uv.js';
9
13
  import { discoverWorkspaceCrates, discoverWorkspacePackages, filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore, validatePackageJson, } from './workspace.js';
10
14
  import.meta.dirname;
11
15
  import * as url from 'url';
12
16
  export { parseImageRef } from "./oci_image/utils.js";
13
17
  export { ImageRef } from "./oci_image/images.js";
14
18
  export { getProjectLicense, findLicenseFilePath, identifyLicense, getLicenseDetails, licensesFromReport, normalizeLicensesResponse, runLicenseCheck, getCompatibility } from "./license/index.js";
15
- export default { componentAnalysis, stackAnalysis, stackAnalysisBatch, imageAnalysis, validateToken };
16
- export { discoverWorkspacePackages, discoverWorkspaceCrates, validatePackageJson, resolveWorkspaceDiscoveryIgnore, filterManifestPathsByDiscoveryIgnore, resolveContinueOnError, resolveBatchMetadata, };
19
+ export default { componentAnalysis, stackAnalysis, stackAnalysisBatch, imageAnalysis, validateToken, generateSbom };
20
+ export { discoverMavenModules, discoverGradleSubprojects, discoverGoWorkspaceModules, discoverUvWorkspaceMembers, discoverWorkspacePackages, discoverWorkspaceCrates, validatePackageJson, resolveWorkspaceDiscoveryIgnore, filterManifestPathsByDiscoveryIgnore, resolveContinueOnError, resolveBatchMetadata, };
17
21
  /**
18
22
  * @typedef {{
19
23
  * TRUSTIFY_DA_CARGO_PATH?: string | undefined,
@@ -56,13 +60,15 @@ export { discoverWorkspacePackages, discoverWorkspaceCrates, validatePackageJson
56
60
  * TRUSTIFY_DA_CONTINUE_ON_ERROR?: string | undefined,
57
61
  * batchMetadata?: boolean | undefined,
58
62
  * TRUSTIFY_DA_BATCH_METADATA?: string | undefined,
63
+ * TRUSTIFY_DA_UV_PATH?: string | undefined,
64
+ * TRUSTIFY_DA_POETRY_PATH?: string | undefined,
59
65
  * [key: string]: string | number | boolean | string[] | undefined,
60
66
  * }} Options
61
67
  */
62
68
  /**
63
69
  * @typedef {{
64
70
  * workspaceRoot: string,
65
- * ecosystem: 'javascript' | 'cargo' | 'unknown',
71
+ * ecosystem: 'javascript' | 'cargo' | 'pyproject' | 'unknown',
66
72
  * total: number,
67
73
  * successful: number,
68
74
  * failed: number,
@@ -235,6 +241,22 @@ function buildBatchAnalysisMetadata(root, ecosystem, totalSbomAttempts, successf
235
241
  errors: [...errors],
236
242
  };
237
243
  }
244
+ /**
245
+ * Generate a CycloneDX SBOM from a manifest file. No backend HTTP request is made.
246
+ *
247
+ * @param {string} manifestPath - path to the manifest file (e.g. pom.xml, package.json)
248
+ * @param {Options} [opts={}] - optional options (e.g. workspace dir, tool paths)
249
+ * @returns {Promise<object>} parsed CycloneDX SBOM JSON object
250
+ * @throws {Error} if the manifest is unsupported or SBOM generation fails
251
+ */
252
+ export async function generateSbom(manifestPath, opts = {}) {
253
+ fs.accessSync(manifestPath, fs.constants.R_OK);
254
+ const result = await generateOneSbom(manifestPath, opts);
255
+ if (!result.ok) {
256
+ throw new Error(`Failed to generate SBOM for ${result.manifestPath}: ${result.reason}`);
257
+ }
258
+ return result.sbom;
259
+ }
238
260
  /**
239
261
  * @typedef {{ ok: true, purl: string, sbom: object } | { ok: false, manifestPath: string, reason: string }} SbomResult
240
262
  */
@@ -261,17 +283,45 @@ async function generateOneSbom(manifestPath, workspaceOpts) {
261
283
  *
262
284
  * @param {string} root - Resolved workspace root
263
285
  * @param {Options} opts
264
- * @returns {Promise<{ ecosystem: 'javascript' | 'cargo' | 'unknown', manifestPaths: string[] }>}
286
+ * @returns {Promise<{ ecosystem: 'javascript' | 'cargo' | 'maven' | 'gradle' | 'gomodules' | 'pyproject' | 'unknown', manifestPaths: string[] }>}
265
287
  * @private
266
288
  */
267
289
  async function detectWorkspaceManifests(root, opts) {
268
290
  const cargoToml = path.join(root, 'Cargo.toml');
269
291
  const cargoLock = path.join(root, 'Cargo.lock');
270
292
  const packageJson = path.join(root, 'package.json');
293
+ const pomXml = path.join(root, 'pom.xml');
271
294
  if (fs.existsSync(cargoToml) && fs.existsSync(cargoLock)) {
272
295
  return { ecosystem: 'cargo', manifestPaths: await discoverWorkspaceCrates(root, opts) };
273
296
  }
274
- const hasJsLock = fs.existsSync(path.join(root, 'pnpm-lock.yaml'))
297
+ if (fs.existsSync(pomXml)) {
298
+ const manifestPaths = await discoverMavenModules(root, opts);
299
+ if (manifestPaths.length > 0) {
300
+ return { ecosystem: 'maven', manifestPaths };
301
+ }
302
+ }
303
+ const hasGradleSettings = fs.existsSync(path.join(root, 'settings.gradle'))
304
+ || fs.existsSync(path.join(root, 'settings.gradle.kts'));
305
+ if (hasGradleSettings) {
306
+ const manifestPaths = await discoverGradleSubprojects(root, opts);
307
+ if (manifestPaths.length > 0) {
308
+ return { ecosystem: 'gradle', manifestPaths };
309
+ }
310
+ }
311
+ if (fs.existsSync(path.join(root, 'go.work'))) {
312
+ const manifestPaths = await discoverGoWorkspaceModules(root, opts);
313
+ if (manifestPaths.length > 0) {
314
+ return { ecosystem: 'gomodules', manifestPaths };
315
+ }
316
+ }
317
+ if (fs.existsSync(path.join(root, 'pyproject.toml')) && fs.existsSync(path.join(root, 'uv.lock'))) {
318
+ const manifestPaths = await discoverUvWorkspaceMembers(root, opts);
319
+ if (manifestPaths.length > 0) {
320
+ return { ecosystem: 'pyproject', manifestPaths };
321
+ }
322
+ }
323
+ const hasJsLock = fs.existsSync(path.join(root, 'bun.lock'))
324
+ || fs.existsSync(path.join(root, 'pnpm-lock.yaml'))
275
325
  || fs.existsSync(path.join(root, 'yarn.lock'))
276
326
  || fs.existsSync(path.join(root, 'package-lock.json'));
277
327
  if (fs.existsSync(packageJson) && hasJsLock) {
@@ -380,12 +430,45 @@ function batchError(message, wantMetadata, metadata) {
380
430
  }
381
431
  return err;
382
432
  }
433
+ /**
434
+ * @overload
435
+ * @param {string} workspaceRoot
436
+ * @param {true} html
437
+ * @param {Options & { batchMetadata: true }} opts
438
+ * @returns {Promise<{ analysis: string, metadata: BatchAnalysisMetadata }>}
439
+ * @throws {Error}
440
+ */
441
+ /**
442
+ * @overload
443
+ * @param {string} workspaceRoot
444
+ * @param {true} html
445
+ * @param {Options & { batchMetadata?: false }} [opts={}]
446
+ * @returns {Promise<string>}
447
+ * @throws {Error}
448
+ */
449
+ /**
450
+ * @overload
451
+ * @param {string} workspaceRoot
452
+ * @param {false} html
453
+ * @param {Options & { batchMetadata: true }} opts
454
+ * @returns {Promise<{ analysis: Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>, metadata: BatchAnalysisMetadata }>}
455
+ * @throws {Error}
456
+ */
457
+ /**
458
+ * @overload
459
+ * @param {string} workspaceRoot
460
+ * @param {false} html
461
+ * @param {Options & { batchMetadata?: false }} [opts={}]
462
+ * @returns {Promise<Object.<string, import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport').AnalysisReport>>}
463
+ * @throws {Error}
464
+ */
383
465
  /**
384
466
  * Get stack analysis for all workspace packages/crates (batch).
385
467
  * Detects ecosystem from workspace root: Cargo (Cargo.toml + Cargo.lock) or JS/TS (package.json + lock file).
386
468
  * SBOMs are generated in parallel (see `batchConcurrency`) unless `continueOnError: false` (fail-fast sequential).
387
469
  * With `opts.batchMetadata` / `TRUSTIFY_DA_BATCH_METADATA`, returns `{ analysis, metadata }` including validation and SBOM errors.
388
470
  *
471
+ * @overload
389
472
  * @param {string} workspaceRoot - Path to workspace root (containing lock file and workspace config)
390
473
  * @param {boolean} [html=false] - true returns HTML, false returns JSON report
391
474
  * @param {Options} [opts={}] - `batchConcurrency`, discovery ignores, `continueOnError` (default true), `batchMetadata` (default false)
@@ -416,7 +499,7 @@ async function stackAnalysisBatch(workspaceRoot, html = false, opts = {}) {
416
499
  }
417
500
  }
418
501
  if (manifestPaths.length === 0) {
419
- throw new Error(`No workspace manifests found at ${root}. Ensure Cargo.toml+Cargo.lock or package.json+lock file exist.`);
502
+ throw new Error(`No workspace manifests found at ${root}. Ensure a supported workspace root exists (Cargo.toml+Cargo.lock, pom.xml, build.gradle, go.work, pyproject.toml+uv.lock, or package.json+lock file).`);
420
503
  }
421
504
  const workspaceOpts = { ...opts, TRUSTIFY_DA_WORKSPACE_DIR: root };
422
505
  const concurrency = resolveBatchConcurrency(opts);
@@ -135,7 +135,7 @@ function execSyft(imageRef, opts = {}) {
135
135
  function getSyftEnvs(dockerPath, podmanPath) {
136
136
  let path = null;
137
137
  if (dockerPath && podmanPath) {
138
- path = `${dockerPath}${File.pathSeparator}${podmanPath}`;
138
+ path = `${dockerPath}${delimiter}${podmanPath}`;
139
139
  }
140
140
  else if (dockerPath) {
141
141
  path = dockerPath;
@@ -276,7 +276,16 @@ function podmanGetVariant(opts = {}) {
276
276
  * @returns {string} - The information
277
277
  */
278
278
  function dockerPodmanInfo(dockerSupplier, podmanSupplier, opts = {}) {
279
- return dockerSupplier(opts) || podmanSupplier(opts);
279
+ try {
280
+ const result = dockerSupplier(opts);
281
+ if (result) {
282
+ return result;
283
+ }
284
+ }
285
+ catch (_) {
286
+ // docker not available, fall through to podman
287
+ }
288
+ return podmanSupplier(opts);
280
289
  }
281
290
  /**
282
291
  * Gets the digests for an image
@@ -3,10 +3,14 @@ import golangGomodulesProvider from './providers/golang_gomodules.js';
3
3
  import Java_gradle_groovy from "./providers/java_gradle_groovy.js";
4
4
  import Java_gradle_kotlin from "./providers/java_gradle_kotlin.js";
5
5
  import Java_maven from "./providers/java_maven.js";
6
+ import Javascript_bun from './providers/javascript_bun.js';
6
7
  import Javascript_npm from './providers/javascript_npm.js';
7
8
  import Javascript_pnpm from './providers/javascript_pnpm.js';
8
9
  import Javascript_yarn from './providers/javascript_yarn.js';
9
10
  import pythonPipProvider from './providers/python_pip.js';
11
+ import Python_pip_pyproject from './providers/python_pip_pyproject.js';
12
+ import Python_poetry from './providers/python_poetry.js';
13
+ import Python_uv from './providers/python_uv.js';
10
14
  import rustCargoProvider from './providers/rust_cargo.js';
11
15
  /** @typedef {{ecosystem: string, contentType: string, content: string}} Provided */
12
16
  /** @typedef {{isSupported: function(string): boolean, validateLockFile: function(string, Object): void, provideComponent: function(string, {}): Provided | Promise<Provided>, provideStack: function(string, {}): Provided | Promise<Provided>, readLicenseFromManifest: function(string): string | null}} Provider */
@@ -18,11 +22,15 @@ export const availableProviders = [
18
22
  new Java_maven(),
19
23
  new Java_gradle_groovy(),
20
24
  new Java_gradle_kotlin(),
25
+ new Javascript_bun(),
21
26
  new Javascript_pnpm(),
22
27
  new Javascript_yarn(),
23
28
  new Javascript_npm(),
24
29
  golangGomodulesProvider,
25
30
  pythonPipProvider,
31
+ new Python_poetry(),
32
+ new Python_uv(),
33
+ new Python_pip_pyproject(),
26
34
  rustCargoProvider
27
35
  ];
28
36
  /**
@@ -57,15 +57,6 @@ export default class Base_Java {
57
57
  * @returns string
58
58
  */
59
59
  selectToolBinary(manifestPath: string, opts: {}): string | null;
60
- /**
61
- *
62
- * @param {string} startingManifest - the path of the manifest from which to start searching for the wrapper
63
- * @param {string} repoRoot - the root of the repository at which point to stop searching for mvnw, derived via git if unset and then fallsback
64
- * to the root of the drive the manifest is on (assumes absolute path is given)
65
- * @returns {string|undefined}
66
- */
67
- traverseForWrapper(startingManifest: string, repoRoot?: string): string | undefined;
68
- normalizePath(thePath: any): string;
69
60
  #private;
70
61
  }
71
62
  export type Provided = import("../provider").Provided;
@@ -1,7 +1,6 @@
1
- import fs from 'node:fs';
2
1
  import path from 'node:path';
3
2
  import { PackageURL } from 'packageurl-js';
4
- import { getCustomPath, getGitRootDir, getWrapperPreference, invokeCommand } from "../tools.js";
3
+ import { getCustomPath, getWrapperPreference, invokeCommand, traverseForWrapper } from "../tools.js";
5
4
  /** @typedef {import('../provider').Provider} */
6
5
  /** @typedef {import('../provider').Provided} Provided */
7
6
  /** @typedef {{name: string, version: string}} Package */
@@ -131,7 +130,7 @@ export default class Base_Java {
131
130
  const toolPath = getCustomPath(this.globalBinary, opts);
132
131
  const useWrapper = getWrapperPreference(this.globalBinary, opts);
133
132
  if (useWrapper) {
134
- const wrapper = this.traverseForWrapper(manifestPath);
133
+ const wrapper = traverseForWrapper(manifestDir, this.localWrapper);
135
134
  if (wrapper !== undefined) {
136
135
  try {
137
136
  this._invokeCommand(wrapper, ['--version'], { cwd: manifestDir });
@@ -156,39 +155,4 @@ export default class Base_Java {
156
155
  }
157
156
  return toolPath;
158
157
  }
159
- /**
160
- *
161
- * @param {string} startingManifest - the path of the manifest from which to start searching for the wrapper
162
- * @param {string} repoRoot - the root of the repository at which point to stop searching for mvnw, derived via git if unset and then fallsback
163
- * to the root of the drive the manifest is on (assumes absolute path is given)
164
- * @returns {string|undefined}
165
- */
166
- traverseForWrapper(startingManifest, repoRoot = undefined) {
167
- const normalizedManifest = this.normalizePath(startingManifest);
168
- const currentDir = this.normalizePath(path.dirname(normalizedManifest));
169
- repoRoot = repoRoot || getGitRootDir(currentDir) || path.parse(normalizedManifest).root;
170
- const wrapperPath = path.join(currentDir, this.localWrapper);
171
- try {
172
- fs.accessSync(wrapperPath, fs.constants.X_OK);
173
- return wrapperPath;
174
- }
175
- catch (error) {
176
- if (error.code === 'ENOENT') {
177
- const rootDir = path.parse(currentDir).root;
178
- if (currentDir === repoRoot || currentDir === rootDir) {
179
- return undefined;
180
- }
181
- const parentDir = path.dirname(currentDir);
182
- if (parentDir === currentDir || parentDir === rootDir) {
183
- return undefined;
184
- }
185
- return this.traverseForWrapper(path.join(parentDir, path.basename(normalizedManifest)), repoRoot);
186
- }
187
- throw new Error(`failure searching for ${this.localWrapper}`, { cause: error });
188
- }
189
- }
190
- normalizePath(thePath) {
191
- const normalized = path.resolve(thePath).normalize();
192
- return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
193
- }
194
158
  }
@@ -136,6 +136,12 @@ export default class Base_javascript {
136
136
  */
137
137
  protected _version(): string;
138
138
  /**
139
+ * Creates or updates the lock file for the package manager
140
+ * @param {string} manifestDir - Directory containing the manifest file
141
+ * @protected
142
+ */
143
+ protected _createLockFile(manifestDir: string): void;
144
+ /**
139
145
  * Parses the dependency tree output
140
146
  * @param {string} output - The output to parse
141
147
  * @returns {string} The parsed output
@@ -217,7 +217,7 @@ export default class Base_javascript {
217
217
  this._version();
218
218
  const manifestDir = path.dirname(this.#manifest.manifestPath);
219
219
  const cmdDir = this._findLockFileDir(manifestDir, opts) || manifestDir;
220
- this.#createLockFile(cmdDir);
220
+ this._createLockFile(cmdDir);
221
221
  let output = this.#executeListCmd(includeTransitive, cmdDir);
222
222
  output = this._parseDepTreeOutput(output);
223
223
  return JSON.parse(output);
@@ -235,9 +235,32 @@ export default class Base_javascript {
235
235
  let sbom = new Sbom();
236
236
  sbom.addRoot(mainComponent, license);
237
237
  this._addDependenciesToSbom(sbom, depsObject);
238
+ this.#ensurePeerAndOptionalDeps(sbom);
238
239
  sbom.filterIgnoredDeps(this.#manifest.ignored);
239
240
  return sbom.getAsJsonString(opts);
240
241
  }
242
+ /**
243
+ * Ensures peer and optional dependencies declared in the manifest are
244
+ * present in the SBOM, even when the package manager does not resolve them
245
+ * (e.g. yarn does not include peer deps in its dependency listing).
246
+ * @param {Sbom} sbom - The SBOM to supplement
247
+ * @private
248
+ */
249
+ #ensurePeerAndOptionalDeps(sbom) {
250
+ const rootPurl = toPurl(purlType, this.#manifest.name, this.#manifest.version);
251
+ const depSources = [this.#manifest.peerDependencies, this.#manifest.optionalDependencies];
252
+ for (const source of depSources) {
253
+ for (const [name, version] of Object.entries(source)) {
254
+ // Build the purl prefix for exact matching (e.g. "pkg:npm/minimist@"
255
+ // or "pkg:npm/%40hapi/joi@") to avoid substring false positives
256
+ const probe = toPurl(purlType, name, version);
257
+ const purlPrefix = probe.toString().replace(/@[^@]*$/, '@');
258
+ if (!sbom.checkDependsOnByPurlPrefix(rootPurl, purlPrefix)) {
259
+ sbom.addDependency(rootPurl, probe);
260
+ }
261
+ }
262
+ }
263
+ }
241
264
  /**
242
265
  * Recursively builds the Sbom from the JSON that npm listing returns
243
266
  * @param {Sbom} sbom - The SBOM object to add dependencies to
@@ -245,7 +268,10 @@ export default class Base_javascript {
245
268
  * @protected
246
269
  */
247
270
  _addDependenciesToSbom(sbom, depTree) {
248
- const dependencies = depTree["dependencies"] || {};
271
+ const dependencies = {
272
+ ...depTree["dependencies"],
273
+ ...depTree["optionalDependencies"],
274
+ };
249
275
  Object.entries(dependencies)
250
276
  .forEach(entry => {
251
277
  const [name, artifact] = entry;
@@ -295,6 +321,7 @@ export default class Base_javascript {
295
321
  const rootPurl = toPurlFromString(sbom.getRoot().purl);
296
322
  sbom.addDependency(rootPurl, rootDeps.get(key));
297
323
  }
324
+ this.#ensurePeerAndOptionalDeps(sbom);
298
325
  sbom.filterIgnoredDeps(this.#manifest.ignored);
299
326
  return sbom.getAsJsonString(opts);
300
327
  }
@@ -305,10 +332,14 @@ export default class Base_javascript {
305
332
  * @protected
306
333
  */
307
334
  _getRootDependencies(depTree) {
308
- if (!depTree.dependencies) {
335
+ const allDeps = {
336
+ ...depTree.dependencies,
337
+ ...depTree.optionalDependencies,
338
+ };
339
+ if (Object.keys(allDeps).length === 0) {
309
340
  return new Map();
310
341
  }
311
- return new Map(Object.entries(depTree.dependencies).map(([key, value]) => [key, toPurl(purlType, key, value.version)]));
342
+ return new Map(Object.entries(allDeps).map(([key, value]) => [key, toPurl(purlType, key, value.version)]));
312
343
  }
313
344
  /**
314
345
  * Executes the list command to get dependencies
@@ -332,9 +363,9 @@ export default class Base_javascript {
332
363
  /**
333
364
  * Creates or updates the lock file for the package manager
334
365
  * @param {string} manifestDir - Directory containing the manifest file
335
- * @private
366
+ * @protected
336
367
  */
337
- #createLockFile(manifestDir) {
368
+ _createLockFile(manifestDir) {
338
369
  const originalDir = process.cwd();
339
370
  const isWindows = os.platform() === 'win32';
340
371
  if (isWindows) {
@@ -0,0 +1,153 @@
1
+ /** @typedef {{name: string, version: string, children: string[], hashes?: Array<{alg: string, content: string}>}} GraphEntry */
2
+ /** @typedef {{name: string, version: string, dependencies: DepTreeEntry[]}} DepTreeEntry */
3
+ /** @typedef {{directDeps: string[], graph: Map<string, GraphEntry>}} DependencyData */
4
+ /** @typedef {{ecosystem: string, content: string, contentType: string}} Provided */
5
+ export default class Base_pyproject {
6
+ /**
7
+ * @param {string} manifestName
8
+ * @returns {boolean}
9
+ */
10
+ isSupported(manifestName: string): boolean;
11
+ /**
12
+ * @param {string} manifestDir
13
+ * @param {Object} [opts={}]
14
+ * @returns {boolean}
15
+ */
16
+ validateLockFile(manifestDir: string, opts?: any): boolean;
17
+ /**
18
+ * Walk up from manifestDir to find the directory containing the lock file.
19
+ * Follows the same pattern as Base_javascript._findLockFileDir().
20
+ * @param {string} manifestDir
21
+ * @param {Object} [opts={}]
22
+ * @returns {string|null}
23
+ * @protected
24
+ */
25
+ protected _findLockFileDir(manifestDir: string, opts?: any): string | null;
26
+ /**
27
+ * Detect workspace root boundaries.
28
+ * Currently only uv has native workspace support ([tool.uv.workspace] in pyproject.toml).
29
+ * Poetry has no workspace/monorepo support (python-poetry/poetry#2270), so each
30
+ * poetry project is treated independently — see Python_poetry._findLockFileDir().
31
+ * @param {string} dir
32
+ * @returns {boolean}
33
+ * @protected
34
+ */
35
+ protected _isWorkspaceRoot(dir: string): boolean;
36
+ /**
37
+ * Read project license from pyproject.toml, with fallback to LICENSE file.
38
+ * @param {string} manifestPath
39
+ * @returns {string|null}
40
+ */
41
+ readLicenseFromManifest(manifestPath: string): string | null;
42
+ /**
43
+ * @param {string} manifest - path to pyproject.toml
44
+ * @param {Object} [opts={}]
45
+ * @returns {Promise<Provided>}
46
+ */
47
+ provideStack(manifest: string, opts?: any): Promise<Provided>;
48
+ /**
49
+ * @param {string} manifest - path to pyproject.toml
50
+ * @param {Object} [opts={}]
51
+ * @returns {Promise<Provided>}
52
+ */
53
+ provideComponent(manifest: string, opts?: any): Promise<Provided>;
54
+ /**
55
+ * @returns {string}
56
+ * @protected
57
+ */
58
+ protected _lockFileName(): string;
59
+ /**
60
+ * @returns {string}
61
+ * @protected
62
+ */
63
+ protected _cmdName(): string;
64
+ /**
65
+ * Resolve dependencies using the tool-specific command and parser.
66
+ *
67
+ * @param {string} manifestDir - directory containing the target pyproject.toml
68
+ * @param {string} workspaceDir - workspace root (where the lock file lives);
69
+ * only used by providers that need workspace-level resolution (e.g. uv)
70
+ * @param {object} parsed - parsed pyproject.toml
71
+ * @param {Object} opts
72
+ * @returns {Promise<DependencyData>}
73
+ * @protected
74
+ */
75
+ protected _getDependencyData(manifestDir: string, workspaceDir: string, parsed: object, opts: any): Promise<DependencyData>;
76
+ /**
77
+ * Canonicalize a Python package name per PEP 503.
78
+ * @param {string} name
79
+ * @returns {string}
80
+ * @protected
81
+ */
82
+ protected _canonicalize(name: string): string;
83
+ /**
84
+ * Get the project name from pyproject.toml.
85
+ * @param {object} parsed
86
+ * @returns {string|null}
87
+ * @protected
88
+ */
89
+ protected _getProjectName(parsed: object): string | null;
90
+ /**
91
+ * Get the project version from pyproject.toml.
92
+ * @param {object} parsed
93
+ * @returns {string|null}
94
+ * @protected
95
+ */
96
+ protected _getProjectVersion(parsed: object): string | null;
97
+ /**
98
+ * Scan raw pyproject.toml text for dependencies with ignore markers.
99
+ * @param {string} manifestPath
100
+ * @returns {Set<string>}
101
+ * @protected
102
+ */
103
+ protected _getIgnoredDeps(manifestPath: string): Set<string>;
104
+ /**
105
+ * Compute the set of graph nodes reachable from direct deps, excluding ignored.
106
+ * @param {Map<string, GraphEntry>} graph
107
+ * @param {string[]} directDeps
108
+ * @param {Set<string>} ignoredDeps
109
+ * @returns {Set<string>}
110
+ * @protected
111
+ */
112
+ protected _reachableNodes(graph: Map<string, GraphEntry>, directDeps: string[], ignoredDeps: Set<string>): Set<string>;
113
+ /**
114
+ * @param {string} name
115
+ * @param {string} version
116
+ * @returns {PackageURL}
117
+ * @protected
118
+ */
119
+ protected _toPurl(name: string, version: string): PackageURL;
120
+ /**
121
+ * Create SBOM json string for a pyproject.toml project.
122
+ * @param {string} manifest - path to pyproject.toml
123
+ * @param {Object} opts
124
+ * @param {boolean} includeTransitive
125
+ * @returns {Promise<string>}
126
+ * @private
127
+ */
128
+ private _createSbom;
129
+ }
130
+ export type GraphEntry = {
131
+ name: string;
132
+ version: string;
133
+ children: string[];
134
+ hashes?: Array<{
135
+ alg: string;
136
+ content: string;
137
+ }>;
138
+ };
139
+ export type DepTreeEntry = {
140
+ name: string;
141
+ version: string;
142
+ dependencies: DepTreeEntry[];
143
+ };
144
+ export type DependencyData = {
145
+ directDeps: string[];
146
+ graph: Map<string, GraphEntry>;
147
+ };
148
+ export type Provided = {
149
+ ecosystem: string;
150
+ content: string;
151
+ contentType: string;
152
+ };
153
+ import { PackageURL } from 'packageurl-js';