@trustify-da/trustify-da-javascript-client 0.3.0-ea.38515a7 → 0.3.0-ea.3aa2054
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/dist/src/cli.js +51 -2
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +17 -1
- package/dist/src/providers/base_pyproject.d.ts +26 -3
- package/dist/src/providers/base_pyproject.js +64 -5
- package/dist/src/providers/python_poetry.js +26 -3
- package/dist/src/providers/python_uv.d.ts +2 -1
- package/dist/src/providers/python_uv.js +32 -4
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import * as path from "path";
|
|
3
4
|
import yargs from 'yargs';
|
|
4
5
|
import { hideBin } from 'yargs/helpers';
|
|
5
6
|
import { getProjectLicense, getLicenseDetails } from './license/index.js';
|
|
6
|
-
import client, { selectTrustifyDABackend } from './index.js';
|
|
7
|
+
import client, { selectTrustifyDABackend, generateSbom } from './index.js';
|
|
7
8
|
// command for component analysis take manifest type and content
|
|
8
9
|
const component = {
|
|
9
10
|
command: 'component </path/to/manifest>',
|
|
@@ -325,15 +326,63 @@ const license = {
|
|
|
325
326
|
console.log(JSON.stringify(output, null, 2));
|
|
326
327
|
}
|
|
327
328
|
};
|
|
329
|
+
const sbom = {
|
|
330
|
+
command: 'sbom </path/to/manifest> [--output]',
|
|
331
|
+
desc: 'generate a CycloneDX SBOM from a manifest file',
|
|
332
|
+
builder: yargs => yargs.positional('/path/to/manifest', {
|
|
333
|
+
desc: 'manifest path for SBOM generation',
|
|
334
|
+
type: 'string',
|
|
335
|
+
normalize: true,
|
|
336
|
+
}).options({
|
|
337
|
+
output: {
|
|
338
|
+
alias: 'o',
|
|
339
|
+
desc: 'Write SBOM JSON to a file instead of stdout',
|
|
340
|
+
type: 'string',
|
|
341
|
+
normalize: true,
|
|
342
|
+
},
|
|
343
|
+
workspaceDir: {
|
|
344
|
+
alias: 'w',
|
|
345
|
+
desc: 'Workspace root directory (for monorepos; lock file is expected here)',
|
|
346
|
+
type: 'string',
|
|
347
|
+
normalize: true,
|
|
348
|
+
}
|
|
349
|
+
}),
|
|
350
|
+
handler: async (args) => {
|
|
351
|
+
let manifest = args['/path/to/manifest'];
|
|
352
|
+
const opts = args.workspaceDir ? { TRUSTIFY_DA_WORKSPACE_DIR: args.workspaceDir } : {};
|
|
353
|
+
let result;
|
|
354
|
+
try {
|
|
355
|
+
result = await generateSbom(manifest, opts);
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
console.error(JSON.stringify({ error: `Failed to generate SBOM: ${err.message}` }, null, 2));
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
const json = JSON.stringify(result, null, 2);
|
|
362
|
+
if (args.output) {
|
|
363
|
+
try {
|
|
364
|
+
fs.writeFileSync(args.output, json);
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
console.error(JSON.stringify({ error: `Failed to write output file: ${err.message}` }, null, 2));
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
console.log(json);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
328
376
|
// parse and invoke the command
|
|
329
377
|
yargs(hideBin(process.argv))
|
|
330
|
-
.usage(`Usage: ${process.argv[0].includes("node") ? path.parse(process.argv[1]).base : path.parse(process.argv[0]).base} {component|stack|stack-batch|image|validate-token|license}`)
|
|
378
|
+
.usage(`Usage: ${process.argv[0].includes("node") ? path.parse(process.argv[1]).base : path.parse(process.argv[0]).base} {component|stack|stack-batch|image|validate-token|license|sbom}`)
|
|
331
379
|
.command(stack)
|
|
332
380
|
.command(stackBatch)
|
|
333
381
|
.command(component)
|
|
334
382
|
.command(image)
|
|
335
383
|
.command(validateToken)
|
|
336
384
|
.command(license)
|
|
385
|
+
.command(sbom)
|
|
337
386
|
.scriptName('')
|
|
338
387
|
.version(false)
|
|
339
388
|
.demandCommand(1)
|
package/dist/src/index.d.ts
CHANGED
|
@@ -13,6 +13,15 @@ export function selectTrustifyDABackend(opts?: {
|
|
|
13
13
|
TRUSTIFY_DA_DEBUG?: string | undefined;
|
|
14
14
|
TRUSTIFY_DA_BACKEND_URL?: string | undefined;
|
|
15
15
|
}): string;
|
|
16
|
+
/**
|
|
17
|
+
* Generate a CycloneDX SBOM from a manifest file. No backend HTTP request is made.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} manifestPath - path to the manifest file (e.g. pom.xml, package.json)
|
|
20
|
+
* @param {Options} [opts={}] - optional options (e.g. workspace dir, tool paths)
|
|
21
|
+
* @returns {Promise<object>} parsed CycloneDX SBOM JSON object
|
|
22
|
+
* @throws {Error} if the manifest is unsupported or SBOM generation fails
|
|
23
|
+
*/
|
|
24
|
+
export function generateSbom(manifestPath: string, opts?: Options): Promise<object>;
|
|
16
25
|
export { parseImageRef } from "./oci_image/utils.js";
|
|
17
26
|
export { ImageRef } from "./oci_image/images.js";
|
|
18
27
|
declare namespace _default {
|
|
@@ -21,6 +30,7 @@ declare namespace _default {
|
|
|
21
30
|
export { stackAnalysisBatch };
|
|
22
31
|
export { imageAnalysis };
|
|
23
32
|
export { validateToken };
|
|
33
|
+
export { generateSbom };
|
|
24
34
|
}
|
|
25
35
|
export default _default;
|
|
26
36
|
export type Options = {
|
package/dist/src/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import * as url from 'url';
|
|
|
12
12
|
export { parseImageRef } from "./oci_image/utils.js";
|
|
13
13
|
export { ImageRef } from "./oci_image/images.js";
|
|
14
14
|
export { getProjectLicense, findLicenseFilePath, identifyLicense, getLicenseDetails, licensesFromReport, normalizeLicensesResponse, runLicenseCheck, getCompatibility } from "./license/index.js";
|
|
15
|
-
export default { componentAnalysis, stackAnalysis, stackAnalysisBatch, imageAnalysis, validateToken };
|
|
15
|
+
export default { componentAnalysis, stackAnalysis, stackAnalysisBatch, imageAnalysis, validateToken, generateSbom };
|
|
16
16
|
export { discoverWorkspacePackages, discoverWorkspaceCrates, validatePackageJson, resolveWorkspaceDiscoveryIgnore, filterManifestPathsByDiscoveryIgnore, resolveContinueOnError, resolveBatchMetadata, };
|
|
17
17
|
/**
|
|
18
18
|
* @typedef {{
|
|
@@ -237,6 +237,22 @@ function buildBatchAnalysisMetadata(root, ecosystem, totalSbomAttempts, successf
|
|
|
237
237
|
errors: [...errors],
|
|
238
238
|
};
|
|
239
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Generate a CycloneDX SBOM from a manifest file. No backend HTTP request is made.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} manifestPath - path to the manifest file (e.g. pom.xml, package.json)
|
|
244
|
+
* @param {Options} [opts={}] - optional options (e.g. workspace dir, tool paths)
|
|
245
|
+
* @returns {Promise<object>} parsed CycloneDX SBOM JSON object
|
|
246
|
+
* @throws {Error} if the manifest is unsupported or SBOM generation fails
|
|
247
|
+
*/
|
|
248
|
+
export async function generateSbom(manifestPath, opts = {}) {
|
|
249
|
+
fs.accessSync(manifestPath, fs.constants.R_OK);
|
|
250
|
+
const result = await generateOneSbom(manifestPath, opts);
|
|
251
|
+
if (!result.ok) {
|
|
252
|
+
throw new Error(`Failed to generate SBOM for ${result.manifestPath}: ${result.reason}`);
|
|
253
|
+
}
|
|
254
|
+
return result.sbom;
|
|
255
|
+
}
|
|
240
256
|
/**
|
|
241
257
|
* @typedef {{ ok: true, purl: string, sbom: object } | { ok: false, manifestPath: string, reason: string }} SbomResult
|
|
242
258
|
*/
|
|
@@ -10,9 +10,29 @@ export default class Base_pyproject {
|
|
|
10
10
|
isSupported(manifestName: string): boolean;
|
|
11
11
|
/**
|
|
12
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
|
|
13
32
|
* @returns {boolean}
|
|
33
|
+
* @protected
|
|
14
34
|
*/
|
|
15
|
-
|
|
35
|
+
protected _isWorkspaceRoot(dir: string): boolean;
|
|
16
36
|
/**
|
|
17
37
|
* Read project license from pyproject.toml, with fallback to LICENSE file.
|
|
18
38
|
* @param {string} manifestPath
|
|
@@ -43,13 +63,16 @@ export default class Base_pyproject {
|
|
|
43
63
|
protected _cmdName(): string;
|
|
44
64
|
/**
|
|
45
65
|
* Resolve dependencies using the tool-specific command and parser.
|
|
46
|
-
*
|
|
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)
|
|
47
70
|
* @param {object} parsed - parsed pyproject.toml
|
|
48
71
|
* @param {Object} opts
|
|
49
72
|
* @returns {Promise<DependencyData>}
|
|
50
73
|
* @protected
|
|
51
74
|
*/
|
|
52
|
-
protected _getDependencyData(manifestDir: string, parsed: object, opts: any): Promise<DependencyData>;
|
|
75
|
+
protected _getDependencyData(manifestDir: string, workspaceDir: string, parsed: object, opts: any): Promise<DependencyData>;
|
|
53
76
|
/**
|
|
54
77
|
* Canonicalize a Python package name per PEP 503.
|
|
55
78
|
* @param {string} name
|
|
@@ -4,6 +4,7 @@ import { PackageURL } from 'packageurl-js';
|
|
|
4
4
|
import { parse as parseToml } from 'smol-toml';
|
|
5
5
|
import { getLicense } from '../license/license_utils.js';
|
|
6
6
|
import Sbom from '../sbom.js';
|
|
7
|
+
import { getCustom } from '../tools.js';
|
|
7
8
|
const ecosystem = 'pip';
|
|
8
9
|
const IGNORE_MARKERS = ['exhortignore', 'trustify-da-ignore'];
|
|
9
10
|
const DEFAULT_ROOT_NAME = 'default-pip-root';
|
|
@@ -22,10 +23,64 @@ export default class Base_pyproject {
|
|
|
22
23
|
}
|
|
23
24
|
/**
|
|
24
25
|
* @param {string} manifestDir
|
|
26
|
+
* @param {Object} [opts={}]
|
|
25
27
|
* @returns {boolean}
|
|
26
28
|
*/
|
|
27
|
-
validateLockFile(manifestDir) {
|
|
28
|
-
return
|
|
29
|
+
validateLockFile(manifestDir, opts = {}) {
|
|
30
|
+
return this._findLockFileDir(manifestDir, opts) != null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Walk up from manifestDir to find the directory containing the lock file.
|
|
34
|
+
* Follows the same pattern as Base_javascript._findLockFileDir().
|
|
35
|
+
* @param {string} manifestDir
|
|
36
|
+
* @param {Object} [opts={}]
|
|
37
|
+
* @returns {string|null}
|
|
38
|
+
* @protected
|
|
39
|
+
*/
|
|
40
|
+
_findLockFileDir(manifestDir, opts = {}) {
|
|
41
|
+
const workspaceDir = getCustom('TRUSTIFY_DA_WORKSPACE_DIR', null, opts);
|
|
42
|
+
if (workspaceDir) {
|
|
43
|
+
const dir = path.resolve(workspaceDir);
|
|
44
|
+
return fs.existsSync(path.join(dir, this._lockFileName())) ? dir : null;
|
|
45
|
+
}
|
|
46
|
+
let dir = path.resolve(manifestDir);
|
|
47
|
+
let parent = dir;
|
|
48
|
+
do {
|
|
49
|
+
dir = parent;
|
|
50
|
+
if (fs.existsSync(path.join(dir, this._lockFileName()))) {
|
|
51
|
+
return dir;
|
|
52
|
+
}
|
|
53
|
+
if (this._isWorkspaceRoot(dir)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
parent = path.dirname(dir);
|
|
57
|
+
} while (parent !== dir);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Detect workspace root boundaries.
|
|
62
|
+
* Currently only uv has native workspace support ([tool.uv.workspace] in pyproject.toml).
|
|
63
|
+
* Poetry has no workspace/monorepo support (python-poetry/poetry#2270), so each
|
|
64
|
+
* poetry project is treated independently — see Python_poetry._findLockFileDir().
|
|
65
|
+
* @param {string} dir
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
* @protected
|
|
68
|
+
*/
|
|
69
|
+
_isWorkspaceRoot(dir) {
|
|
70
|
+
const pyprojectPath = path.join(dir, 'pyproject.toml');
|
|
71
|
+
if (!fs.existsSync(pyprojectPath)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const content = parseToml(fs.readFileSync(pyprojectPath, 'utf-8'));
|
|
76
|
+
if (content.tool?.uv?.workspace) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (_) {
|
|
81
|
+
// ignore parse errors
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
29
84
|
}
|
|
30
85
|
/**
|
|
31
86
|
* Read project license from pyproject.toml, with fallback to LICENSE file.
|
|
@@ -91,14 +146,17 @@ export default class Base_pyproject {
|
|
|
91
146
|
}
|
|
92
147
|
/**
|
|
93
148
|
* Resolve dependencies using the tool-specific command and parser.
|
|
94
|
-
*
|
|
149
|
+
*
|
|
150
|
+
* @param {string} manifestDir - directory containing the target pyproject.toml
|
|
151
|
+
* @param {string} workspaceDir - workspace root (where the lock file lives);
|
|
152
|
+
* only used by providers that need workspace-level resolution (e.g. uv)
|
|
95
153
|
* @param {object} parsed - parsed pyproject.toml
|
|
96
154
|
* @param {Object} opts
|
|
97
155
|
* @returns {Promise<DependencyData>}
|
|
98
156
|
* @protected
|
|
99
157
|
*/
|
|
100
158
|
// eslint-disable-next-line no-unused-vars
|
|
101
|
-
async _getDependencyData(manifestDir, parsed, opts) {
|
|
159
|
+
async _getDependencyData(manifestDir, workspaceDir, parsed, opts) {
|
|
102
160
|
throw new TypeError('_getDependencyData must be implemented');
|
|
103
161
|
}
|
|
104
162
|
// --- shared helpers ---
|
|
@@ -257,7 +315,8 @@ export default class Base_pyproject {
|
|
|
257
315
|
let manifestDir = path.dirname(manifest);
|
|
258
316
|
let content = fs.readFileSync(manifest, 'utf-8');
|
|
259
317
|
let parsed = parseToml(content);
|
|
260
|
-
let
|
|
318
|
+
let workspaceDir = this._findLockFileDir(manifestDir, opts) || manifestDir;
|
|
319
|
+
let { directDeps, graph } = await this._getDependencyData(manifestDir, workspaceDir, parsed, opts);
|
|
261
320
|
let ignoredDeps = this._getIgnoredDeps(manifest);
|
|
262
321
|
let dependencies = this._buildDependencyTree(graph, directDeps, ignoredDeps, includeTransitive);
|
|
263
322
|
let sbom = new Sbom();
|
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { environmentVariableIsPopulated, getCustom, getCustomPath, invokeCommand } from '../tools.js';
|
|
2
4
|
import Base_pyproject from './base_pyproject.js';
|
|
3
5
|
export default class Python_poetry extends Base_pyproject {
|
|
6
|
+
/**
|
|
7
|
+
* Poetry has no native workspace/monorepo support (python-poetry/poetry#2270).
|
|
8
|
+
* Each poetry project is treated independently — no lock file walk-up.
|
|
9
|
+
* Running `poetry show` from a parent directory returns the parent's deps, not
|
|
10
|
+
* the sub-package's, so walk-up would produce incorrect SBOMs.
|
|
11
|
+
* @param {string} manifestDir
|
|
12
|
+
* @param {Object} [opts={}]
|
|
13
|
+
* @returns {string|null}
|
|
14
|
+
* @protected
|
|
15
|
+
*/
|
|
16
|
+
_findLockFileDir(manifestDir, opts = {}) {
|
|
17
|
+
const workspaceDir = getCustom('TRUSTIFY_DA_WORKSPACE_DIR', null, opts);
|
|
18
|
+
if (workspaceDir) {
|
|
19
|
+
const dir = path.resolve(workspaceDir);
|
|
20
|
+
return fs.existsSync(path.join(dir, this._lockFileName())) ? dir : null;
|
|
21
|
+
}
|
|
22
|
+
const dir = path.resolve(manifestDir);
|
|
23
|
+
return fs.existsSync(path.join(dir, this._lockFileName())) ? dir : null;
|
|
24
|
+
}
|
|
4
25
|
/** @returns {string} */
|
|
5
26
|
_lockFileName() {
|
|
6
27
|
return 'poetry.lock';
|
|
@@ -11,11 +32,13 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
11
32
|
}
|
|
12
33
|
/**
|
|
13
34
|
* @param {string} manifestDir
|
|
35
|
+
* @param {string} _workspaceDir - unused (poetry has no workspace support)
|
|
14
36
|
* @param {object} parsed - parsed pyproject.toml
|
|
15
37
|
* @param {Object} opts
|
|
16
38
|
* @returns {Promise<{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}>}
|
|
17
39
|
*/
|
|
18
|
-
|
|
40
|
+
// eslint-disable-next-line no-unused-vars
|
|
41
|
+
async _getDependencyData(manifestDir, _workspaceDir, parsed, opts) {
|
|
19
42
|
let treeOutput = this._getPoetryShowTreeOutput(manifestDir, opts);
|
|
20
43
|
let showAllOutput = this._getPoetryShowAllOutput(manifestDir, opts);
|
|
21
44
|
let versionMap = this._parsePoetryShowAll(showAllOutput);
|
|
@@ -89,7 +112,7 @@ export default class Python_poetry extends Base_pyproject {
|
|
|
89
112
|
continue;
|
|
90
113
|
}
|
|
91
114
|
// top-level line: "name version description..."
|
|
92
|
-
let topMatch = line.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)\s+(\S+)
|
|
115
|
+
let topMatch = line.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)\s+(\S+)(?:\s|$)/);
|
|
93
116
|
if (topMatch) {
|
|
94
117
|
let name = topMatch[1];
|
|
95
118
|
let version = topMatch[2];
|
|
@@ -12,9 +12,10 @@ export default class Python_uv extends Base_pyproject {
|
|
|
12
12
|
*
|
|
13
13
|
* @param {string} output
|
|
14
14
|
* @param {string} projectName - canonical project name to identify direct deps
|
|
15
|
+
* @param {string} workspaceDir - workspace root (for resolving editable install paths)
|
|
15
16
|
* @returns {Promise<{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}>}
|
|
16
17
|
*/
|
|
17
|
-
_parseUvExport(output: string, projectName: string): Promise<{
|
|
18
|
+
_parseUvExport(output: string, projectName: string, workspaceDir: string): Promise<{
|
|
18
19
|
directDeps: string[];
|
|
19
20
|
graph: Map<string, {
|
|
20
21
|
name: string;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse as parseToml } from 'smol-toml';
|
|
1
4
|
import { environmentVariableIsPopulated, getCustomPath, invokeCommand } from '../tools.js';
|
|
2
5
|
import Base_pyproject from './base_pyproject.js';
|
|
3
6
|
import { getParser, getPinnedVersionQuery } from './requirements_parser.js';
|
|
@@ -11,15 +14,16 @@ export default class Python_uv extends Base_pyproject {
|
|
|
11
14
|
return 'uv';
|
|
12
15
|
}
|
|
13
16
|
/**
|
|
14
|
-
* @param {string} manifestDir
|
|
17
|
+
* @param {string} manifestDir - directory containing the target pyproject.toml
|
|
18
|
+
* @param {string} workspaceDir - workspace root (for resolving editable install paths)
|
|
15
19
|
* @param {object} parsed - parsed pyproject.toml
|
|
16
20
|
* @param {Object} opts
|
|
17
21
|
* @returns {Promise<{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}>}
|
|
18
22
|
*/
|
|
19
|
-
async _getDependencyData(manifestDir, parsed, opts) {
|
|
23
|
+
async _getDependencyData(manifestDir, workspaceDir, parsed, opts) {
|
|
20
24
|
let projectName = this._getProjectName(parsed);
|
|
21
25
|
let uvOutput = this._getUvExportOutput(manifestDir, opts);
|
|
22
|
-
return this._parseUvExport(uvOutput, projectName);
|
|
26
|
+
return this._parseUvExport(uvOutput, projectName, workspaceDir);
|
|
23
27
|
}
|
|
24
28
|
/**
|
|
25
29
|
* Get the uv export output, either from env var or by running the command.
|
|
@@ -40,9 +44,10 @@ export default class Python_uv extends Base_pyproject {
|
|
|
40
44
|
*
|
|
41
45
|
* @param {string} output
|
|
42
46
|
* @param {string} projectName - canonical project name to identify direct deps
|
|
47
|
+
* @param {string} workspaceDir - workspace root (for resolving editable install paths)
|
|
43
48
|
* @returns {Promise<{directDeps: string[], graph: Map<string, {name: string, version: string, children: string[]}>}>}
|
|
44
49
|
*/
|
|
45
|
-
async _parseUvExport(output, projectName) {
|
|
50
|
+
async _parseUvExport(output, projectName, workspaceDir) {
|
|
46
51
|
let [parser, pinnedVersionQuery] = await Promise.all([
|
|
47
52
|
getParser(), getPinnedVersionQuery()
|
|
48
53
|
]);
|
|
@@ -53,6 +58,29 @@ export default class Python_uv extends Base_pyproject {
|
|
|
53
58
|
let currentPkg = null;
|
|
54
59
|
let collectingVia = false;
|
|
55
60
|
for (let child of root.children) {
|
|
61
|
+
if (child.type === 'global_opt') {
|
|
62
|
+
let optNode = child.children.find(c => c.type === 'option');
|
|
63
|
+
let pathNode = child.children.find(c => c.type === 'path');
|
|
64
|
+
if (optNode?.text === '-e' && pathNode && workspaceDir) {
|
|
65
|
+
let memberDir = path.resolve(workspaceDir, pathNode.text);
|
|
66
|
+
let memberManifest = path.join(memberDir, 'pyproject.toml');
|
|
67
|
+
if (fs.existsSync(memberManifest)) {
|
|
68
|
+
let memberParsed = parseToml(fs.readFileSync(memberManifest, 'utf-8'));
|
|
69
|
+
let name = memberParsed.project?.name || memberParsed.tool?.poetry?.name;
|
|
70
|
+
let version = memberParsed.project?.version || memberParsed.tool?.poetry?.version;
|
|
71
|
+
if (name && version) {
|
|
72
|
+
let key = this._canonicalize(name);
|
|
73
|
+
currentPkg = { name, version, parents: new Set() };
|
|
74
|
+
packages.set(key, currentPkg);
|
|
75
|
+
collectingVia = false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
currentPkg = null;
|
|
81
|
+
collectingVia = false;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
56
84
|
if (child.type === 'requirement') {
|
|
57
85
|
let nameNode = child.children.find(c => c.type === 'package');
|
|
58
86
|
if (!nameNode) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trustify-da/trustify-da-javascript-client",
|
|
3
|
-
"version": "0.3.0-ea.
|
|
3
|
+
"version": "0.3.0-ea.3aa2054",
|
|
4
4
|
"description": "Code-Ready Dependency Analytics JavaScript API.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/guacsec/trustify-da-javascript-client#README.md",
|