Skip to content

Commit a300d13

Browse files
committed
feat: add support for targetting specific workspaces
Signed-off-by: MalickBurger <[email protected]>
1 parent b93b1c3 commit a300d13

File tree

4 files changed

+142
-40
lines changed

4 files changed

+142
-40
lines changed

Diff for: README.md

+44-38
Original file line numberDiff line numberDiff line change
@@ -74,46 +74,52 @@ Usage: cyclonedx-npm [options] [--] [<package-manifest>]
7474
Create CycloneDX Software Bill of Materials (SBOM) from Node.js NPM projects.
7575
7676
Arguments:
77-
<package-manifest> Path to project's manifest file.
78-
(default: "package.json" file in current working directory)
77+
<package-manifest> Path to project's manifest file.
78+
(default: "package.json" file in current working directory)
7979
8080
Options:
81-
--ignore-npm-errors Whether to ignore errors of NPM.
82-
This might be used, if "npm install" was run with "--force" or "--legacy-peer-deps".
83-
(default: false)
84-
--package-lock-only Whether to only use the lock file, ignoring "node_modules".
85-
This means the output will be based only on the few details in and the tree described by the "npm-shrinkwrap.json" or "package-lock.json", rather than the contents of "node_modules" directory.
86-
(default: false)
87-
--omit <type...> Dependency types to omit from the installation tree.
88-
(can be set multiple times)
89-
(choices: "dev", "optional", "peer", default: "dev" if the NODE_ENV environment variable is set to "production", otherwise empty)
90-
--gather-license-texts Search for license files in components and include them as license evidence.
91-
This feature is experimental. (default: false)
92-
--flatten-components Whether to flatten the components.
93-
This means the actual nesting of node packages is not represented in the SBOM result.
94-
(default: false)
95-
--short-PURLs Omit all qualifiers from PackageURLs.
96-
This causes information loss in trade-off shorter PURLs, which might improve ingesting these strings.
97-
(default: false)
98-
--spec-version <version> Which version of CycloneDX spec to use.
99-
(choices: "1.2", "1.3", "1.4", "1.5", "1.6", default: "1.4")
100-
--output-reproducible Whether to go the extra mile and make the output reproducible.
101-
This requires more resources, and might result in loss of time- and random-based-values.
102-
(env: BOM_REPRODUCIBLE)
103-
--output-format <format> Which output format to use.
104-
(choices: "JSON", "XML", default: "JSON")
105-
--output-file <file> Path to the output file.
106-
Set to "-" to write to STDOUT.
107-
(default: write to STDOUT)
108-
--validate Validate resulting BOM before outputting.
109-
Validation is skipped, if requirements not met. See the README.
110-
--no-validate Disable validation of resulting BOM.
111-
--mc-type <type> Type of the main component.
112-
(choices: "application", "firmware", "library", default: "application")
113-
-v, --verbose Increase the verbosity of messages.
114-
Use multiple times to increase the verbosity even more.
115-
-V, --version output the version number
116-
-h, --help display help for command
81+
--ignore-npm-errors Whether to ignore errors of NPM.
82+
This might be used, if "npm install" was run with "--force" or "--legacy-peer-deps".
83+
(default: false)
84+
--package-lock-only Whether to only use the lock file, ignoring "node_modules".
85+
This means the output will be based only on the few details in and the tree described by the "npm-shrinkwrap.json" or "package-lock.json", rather than the contents of "node_modules" directory.
86+
(default: false)
87+
--omit <type...> Dependency types to omit from the installation tree.
88+
(can be set multiple times)
89+
(choices: "dev", "optional", "peer", default: "dev" if the NODE_ENV environment variable is set to "production", otherwise empty)
90+
--gather-license-texts Search for license files in components and include them as license evidence.
91+
This feature is experimental. (default: false)
92+
--flatten-components Whether to flatten the components.
93+
This means the actual nesting of node packages is not represented in the SBOM result.
94+
(default: false)
95+
--short-PURLs Omit all qualifiers from PackageURLs.
96+
This causes information loss in trade-off shorter PURLs, which might improve ingesting these strings.
97+
(default: false)
98+
--spec-version <version> Which version of CycloneDX spec to use.
99+
(choices: "1.2", "1.3", "1.4", "1.5", "1.6", default: "1.4")
100+
--output-reproducible Whether to go the extra mile and make the output reproducible.
101+
This requires more resources, and might result in loss of time- and random-based-values.
102+
(env: BOM_REPRODUCIBLE)
103+
--output-format <format> Which output format to use.
104+
(choices: "JSON", "XML", default: "JSON")
105+
--output-file <file> Path to the output file.
106+
Set to "-" to write to STDOUT.
107+
(default: write to STDOUT)
108+
--validate Validate resulting BOM before outputting.
109+
Validation is skipped, if requirements not met. See the README.
110+
--no-validate Disable validation of resulting BOM.
111+
--mc-type <type> Type of the main component.
112+
(choices: "application", "firmware", "library", default: "application")
113+
-w --workspace <workspace...> Whether to only include dependencies for specific workspaces.
114+
(can be set multiple times)
115+
(default: empty)
116+
-wr --include-workspace-root Include the workspace root when workspaces are defined using "-w" or "--workspace".
117+
(default: false)
118+
-no-ws --no-workspaces Do not include dependencies for workspaces.
119+
-v, --verbose Increase the verbosity of messages.
120+
Use multiple times to increase the verbosity even more.
121+
-V, --version output the version number
122+
-h, --help display help for command
117123
```
118124

119125
## Demo

Diff for: src/builders.ts

+35
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ interface BomBuilderOptions {
4545
flattenComponents?: BomBuilder['flattenComponents']
4646
shortPURLs?: BomBuilder['shortPURLs']
4747
gatherLicenseTexts?: BomBuilder['gatherLicenseTexts']
48+
workspace?: BomBuilder['workspace']
49+
includeWorkspaceRoot?: BomBuilder['includeWorkspaceRoot']
50+
workspaces?: BomBuilder['workspaces']
4851
}
4952

5053
type cPath = string
@@ -65,6 +68,9 @@ export class BomBuilder {
6568
flattenComponents: boolean
6669
shortPURLs: boolean
6770
gatherLicenseTexts: boolean
71+
workspace: string[]
72+
includeWorkspaceRoot: boolean
73+
workspaces: boolean
6874

6975
console: Console
7076

@@ -89,6 +95,9 @@ export class BomBuilder {
8995
this.flattenComponents = options.flattenComponents ?? false
9096
this.shortPURLs = options.shortPURLs ?? false
9197
this.gatherLicenseTexts = options.gatherLicenseTexts ?? false
98+
this.workspace = options.workspace ?? []
99+
this.includeWorkspaceRoot = options.includeWorkspaceRoot ?? false
100+
this.workspaces = options.workspaces ?? true
92101

93102
this.console = console_
94103
}
@@ -175,6 +184,32 @@ export class BomBuilder {
175184
}
176185
}
177186

187+
for (const workspace of this.workspace) {
188+
if (npmVersionT[0] >= 7) {
189+
args.push(`--workspace=${workspace}`)
190+
} else {
191+
this.console.warn('WARN | your NPM does not support "--workspace=%s", internally skipped this option', workspace)
192+
}
193+
}
194+
195+
// No need to set explicitly if false as this is default behaviour
196+
if (this.includeWorkspaceRoot) {
197+
if (npmVersionT[0] >= 8) {
198+
args.push('--include-workspace-root=true')
199+
} else {
200+
this.console.warn('WARN | your NPM does not support "--include-workspace-root=true", internally skipped this option')
201+
}
202+
}
203+
204+
// No need to set explicitly if true as this is default behaviour
205+
if (!this.workspaces) {
206+
if (npmVersionT[0] >= 7) {
207+
args.push('--workspaces=false')
208+
} else {
209+
this.console.warn('WARN | your NPM does not support "--workspaces=false", internally skipped this option')
210+
}
211+
}
212+
178213
this.console.info('INFO | gathering dependency tree ...')
179214
this.console.debug('DEBUG | npm-ls: run npm with %j in %j', args, projectDir)
180215
let npmLsReturns: Buffer

Diff for: src/cli.ts

+37-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ interface CommandOptions {
4343
ignoreNpmErrors: boolean
4444
packageLockOnly: boolean
4545
omit: Omittable[]
46+
workspace: string[]
47+
includeWorkspaceRoot: boolean
48+
workspaces: boolean | undefined
4649
gatherLicenseTexts: boolean
4750
flattenComponents: boolean
4851
shortPURLs: boolean
@@ -87,6 +90,22 @@ function makeCommand (process: NodeJS.Process): Command {
8790
: [],
8891
`"${Omittable.Dev}" if the NODE_ENV environment variable is set to "production", otherwise empty`
8992
)
93+
).addOption(
94+
new Option(
95+
'-w, --workspace <workspace...>',
96+
'Whether to only include dependencies for a specific workspace. ' +
97+
'(can be set multiple times)'
98+
).default([], 'empty')
99+
).addOption(
100+
new Option(
101+
'-no-ws, --no-workspaces',
102+
'Do not include dependencies for workspaces.'
103+
)
104+
).addOption(
105+
new Option(
106+
'-wr, --include-workspace-root',
107+
'Include the workspace root when workspaces are defined using `-w` or `--workspace`.'
108+
).default(false)
90109
).addOption(
91110
new Option(
92111
'--gather-license-texts',
@@ -238,6 +257,20 @@ export async function run (process: NodeJS.Process): Promise<number> {
238257
throw new Error('missing evidence')
239258
}
240259

260+
if (options.workspaces !== undefined && !options.workspaces) {
261+
if (options.workspace !== undefined && options.workspace.length > 0) {
262+
myConsole.error('ERROR | Bad config: `--workspace` option cannot be used when `--no-workspaces` is also configured')
263+
throw new Error('bad config')
264+
}
265+
}
266+
267+
if (options.includeWorkspaceRoot) {
268+
if (options.workspace.length === 0) {
269+
myConsole.error('ERROR | Bad config: `--include-workspace-root` can only be used when `--workspace` is also configured')
270+
throw new Error('bad config')
271+
}
272+
}
273+
241274
const extRefFactory = new Factories.FromNodePackageJson.ExternalReferenceFactory()
242275

243276
myConsole.log('LOG | gathering BOM data ...')
@@ -257,7 +290,10 @@ export async function run (process: NodeJS.Process): Promise<number> {
257290
gatherLicenseTexts: options.gatherLicenseTexts,
258291
reproducible: options.outputReproducible,
259292
flattenComponents: options.flattenComponents,
260-
shortPURLs: options.shortPURLs
293+
shortPURLs: options.shortPURLs,
294+
workspace: options.workspace,
295+
includeWorkspaceRoot: options.includeWorkspaceRoot,
296+
workspaces: options.workspaces
261297
},
262298
myConsole
263299
).buildFromProjectDir(projectDir, process)

Diff for: tests/integration/cli.args-pass-through.test.js

+26-1
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,33 @@ describe('integration.cli.args-pass-through', () => {
7070
['package-lock-only npm 8', `8.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm8ArgsGeneral, '--package-lock-only']],
7171
['package-lock-only npm 9', `9.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm9ArgsGeneral, '--package-lock-only']],
7272
['package-lock-only npm 10', `10.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm10ArgsGeneral, '--package-lock-only']],
73-
['package-lock-only npm 11', `11.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm11ArgsGeneral, '--package-lock-only']]
73+
['package-lock-only npm 11', `11.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm11ArgsGeneral, '--package-lock-only']],
7474
// endregion package-lock-only
75+
// region workspace
76+
['workspace not supported npm 6', `6.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm6ArgsGeneral]],
77+
['workspace npm 7', `7.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm7ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']],
78+
['workspace npm 8', `8.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm8ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']],
79+
['workspace npm 9', `9.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm9ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']],
80+
['workspace npm 10', `10.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm10ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']],
81+
['workspace npm 11', `11.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm11ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']],
82+
// endregion workspace
83+
// region include-workspace-root
84+
['workspace root not supported npm 6', `6.${rMinor}.${rPatch}`, ['-w', 'my-wsA', '--include-workspace-root'], [...npm6ArgsGeneral]],
85+
['workspace root not supported npm 7', `7.${rMinor}.${rPatch}`, ['-w', 'my-wsA', '--include-workspace-root'], [...npm7ArgsGeneral]],
86+
['workspace root npm 7', `7.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB', '--include-workspace-root'], [...npm7ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB', '--include-workspace-root=true']],
87+
['workspace root npm 8', `8.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB', '--include-workspace-root'], [...npm8ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB', '--include-workspace-root=true']],
88+
['workspace root npm 9', `9.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB', '--include-workspace-root'], [...npm9ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB', '--include-workspace-root=true']],
89+
['workspace root npm 10', `10.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB', '--include-workspace-root'], [...npm10ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB', '--include-workspace-root=true']],
90+
['workspace root npm 11', `11.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB', '--include-workspace-root'], [...npm11ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB', '--include-workspace-root=true']],
91+
// endregion include-workspace-root
92+
// region workspaces
93+
['workspaces disabled not supported npm 6', `6.${rMinor}.${rPatch}`, ['--no-workspaces'], [...npm6ArgsGeneral]],
94+
['workspaces disabled npm 7', `7.${rMinor}.${rPatch}`, ['--no-workspaces'], [...npm7ArgsGeneral, '--workspaces=false']],
95+
['workspaces disabled npm 8', `8.${rMinor}.${rPatch}`, ['-no-ws'], [...npm8ArgsGeneral, '--workspaces=false']],
96+
['workspaces disabled npm 9', `9.${rMinor}.${rPatch}`, ['--no-workspaces'], [...npm9ArgsGeneral, '--workspaces=false']],
97+
['workspaces disabled npm 10', `10.${rMinor}.${rPatch}`, ['-no-ws'], [...npm10ArgsGeneral, '--workspaces=false']],
98+
['workspaces disabled npm 11', `11.${rMinor}.${rPatch}`, ['-no-ws'], [...npm11ArgsGeneral, '--workspaces=false']]
99+
// endregion workspaces
75100
])('%s', async (purpose, npmVersion, cdxArgs, expectedArgs) => {
76101
const logFileBase = join(tmpRootRun, purpose.replace(/\W/g, '_'))
77102
const cwd = dummyProjectsRoot

0 commit comments

Comments
 (0)