Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for targetting specific workspaces #1212

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 44 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,46 +74,52 @@ Usage: cyclonedx-npm [options] [--] [<package-manifest>]
Create CycloneDX Software Bill of Materials (SBOM) from Node.js NPM projects.

Arguments:
<package-manifest> Path to project's manifest file.
(default: "package.json" file in current working directory)
<package-manifest> Path to project's manifest file.
(default: "package.json" file in current working directory)

Options:
--ignore-npm-errors Whether to ignore errors of NPM.
This might be used, if "npm install" was run with "--force" or "--legacy-peer-deps".
(default: false)
--package-lock-only Whether to only use the lock file, ignoring "node_modules".
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.
(default: false)
--omit <type...> Dependency types to omit from the installation tree.
(can be set multiple times)
(choices: "dev", "optional", "peer", default: "dev" if the NODE_ENV environment variable is set to "production", otherwise empty)
--gather-license-texts Search for license files in components and include them as license evidence.
This feature is experimental. (default: false)
--flatten-components Whether to flatten the components.
This means the actual nesting of node packages is not represented in the SBOM result.
(default: false)
--short-PURLs Omit all qualifiers from PackageURLs.
This causes information loss in trade-off shorter PURLs, which might improve ingesting these strings.
(default: false)
--spec-version <version> Which version of CycloneDX spec to use.
(choices: "1.2", "1.3", "1.4", "1.5", "1.6", default: "1.4")
--output-reproducible Whether to go the extra mile and make the output reproducible.
This requires more resources, and might result in loss of time- and random-based-values.
(env: BOM_REPRODUCIBLE)
--output-format <format> Which output format to use.
(choices: "JSON", "XML", default: "JSON")
--output-file <file> Path to the output file.
Set to "-" to write to STDOUT.
(default: write to STDOUT)
--validate Validate resulting BOM before outputting.
Validation is skipped, if requirements not met. See the README.
--no-validate Disable validation of resulting BOM.
--mc-type <type> Type of the main component.
(choices: "application", "firmware", "library", default: "application")
-v, --verbose Increase the verbosity of messages.
Use multiple times to increase the verbosity even more.
-V, --version output the version number
-h, --help display help for command
--ignore-npm-errors Whether to ignore errors of NPM.
This might be used, if "npm install" was run with "--force" or "--legacy-peer-deps".
(default: false)
--package-lock-only Whether to only use the lock file, ignoring "node_modules".
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.
(default: false)
--omit <type...> Dependency types to omit from the installation tree.
(can be set multiple times)
(choices: "dev", "optional", "peer", default: "dev" if the NODE_ENV environment variable is set to "production", otherwise empty)
--gather-license-texts Search for license files in components and include them as license evidence.
This feature is experimental. (default: false)
--flatten-components Whether to flatten the components.
This means the actual nesting of node packages is not represented in the SBOM result.
(default: false)
--short-PURLs Omit all qualifiers from PackageURLs.
This causes information loss in trade-off shorter PURLs, which might improve ingesting these strings.
(default: false)
--spec-version <version> Which version of CycloneDX spec to use.
(choices: "1.2", "1.3", "1.4", "1.5", "1.6", default: "1.4")
--output-reproducible Whether to go the extra mile and make the output reproducible.
This requires more resources, and might result in loss of time- and random-based-values.
(env: BOM_REPRODUCIBLE)
--output-format <format> Which output format to use.
(choices: "JSON", "XML", default: "JSON")
--output-file <file> Path to the output file.
Set to "-" to write to STDOUT.
(default: write to STDOUT)
--validate Validate resulting BOM before outputting.
Validation is skipped, if requirements not met. See the README.
--no-validate Disable validation of resulting BOM.
--mc-type <type> Type of the main component.
(choices: "application", "firmware", "library", default: "application")
-w --workspace <workspace...> Whether to only include dependencies for specific workspaces.
(can be set multiple times)
(default: empty)
--include-workspace-root Include the workspace root when workspaces are defined using "-w" or "--workspace".
(default: false)
--no-workspaces Do not include dependencies for workspaces.
-v, --verbose Increase the verbosity of messages.
Use multiple times to increase the verbosity even more.
-V, --version output the version number
-h, --help display help for command
```

## Demo
Expand Down
35 changes: 35 additions & 0 deletions src/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ interface BomBuilderOptions {
flattenComponents?: BomBuilder['flattenComponents']
shortPURLs?: BomBuilder['shortPURLs']
gatherLicenseTexts?: BomBuilder['gatherLicenseTexts']
workspace?: BomBuilder['workspace']
includeWorkspaceRoot?: BomBuilder['includeWorkspaceRoot']
workspaces?: BomBuilder['workspaces']
}

type cPath = string
Expand All @@ -65,6 +68,9 @@ export class BomBuilder {
flattenComponents: boolean
shortPURLs: boolean
gatherLicenseTexts: boolean
workspace: string[]
includeWorkspaceRoot: boolean
workspaces: boolean

console: Console

Expand All @@ -89,6 +95,9 @@ export class BomBuilder {
this.flattenComponents = options.flattenComponents ?? false
this.shortPURLs = options.shortPURLs ?? false
this.gatherLicenseTexts = options.gatherLicenseTexts ?? false
this.workspace = options.workspace ?? []
this.includeWorkspaceRoot = options.includeWorkspaceRoot ?? false
this.workspaces = options.workspaces ?? true

this.console = console_
}
Expand Down Expand Up @@ -175,6 +184,32 @@ export class BomBuilder {
}
}

for (const workspace of this.workspace) {
if (npmVersionT[0] >= 7) {
args.push(`--workspace=${workspace}`)
} else {
this.console.warn('WARN | your NPM does not support "--workspace=%s", internally skipped this option', workspace)
}
}

// No need to set explicitly if false as this is default behaviour
if (this.includeWorkspaceRoot) {
if (npmVersionT[0] >= 8) {
args.push('--include-workspace-root=true')
} else {
this.console.warn('WARN | your NPM does not support "--include-workspace-root=true", internally skipped this option')
}
}

// No need to set explicitly if true as this is default behaviour
if (!this.workspaces) {
if (npmVersionT[0] >= 7) {
args.push('--workspaces=false')
} else {
this.console.warn('WARN | your NPM does not support "--workspaces=false", internally skipped this option')
}
}
jkowalleck marked this conversation as resolved.
Show resolved Hide resolved

this.console.info('INFO | gathering dependency tree ...')
this.console.debug('DEBUG | npm-ls: run npm with %j in %j', args, projectDir)
let npmLsReturns: Buffer
Expand Down
38 changes: 37 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ interface CommandOptions {
ignoreNpmErrors: boolean
packageLockOnly: boolean
omit: Omittable[]
workspace: string[]
includeWorkspaceRoot: boolean
workspaces: boolean | undefined
gatherLicenseTexts: boolean
flattenComponents: boolean
shortPURLs: boolean
Expand Down Expand Up @@ -87,6 +90,22 @@ function makeCommand (process: NodeJS.Process): Command {
: [],
`"${Omittable.Dev}" if the NODE_ENV environment variable is set to "production", otherwise empty`
)
).addOption(
new Option(
'-w, --workspace <workspace...>',
'Whether to only include dependencies for a specific workspace. ' +
'(can be set multiple times)'
).default([], 'empty')
).addOption(
new Option(
'--no-workspaces',
'Do not include dependencies for workspaces.'
)
).addOption(
new Option(
'--include-workspace-root',
'Include the workspace root when workspaces are defined using `-w` or `--workspace`.'
).default(false)
).addOption(
new Option(
'--gather-license-texts',
Expand Down Expand Up @@ -238,6 +257,20 @@ export async function run (process: NodeJS.Process): Promise<number> {
throw new Error('missing evidence')
}

if (options.workspaces !== undefined && !options.workspaces) {
if (options.workspace !== undefined && options.workspace.length > 0) {
myConsole.error('ERROR | Bad config: `--workspace` option cannot be used when `--no-workspaces` is also configured')
throw new Error('bad config')
}
}

if (options.includeWorkspaceRoot) {
if (options.workspace.length === 0) {
myConsole.error('ERROR | Bad config: `--include-workspace-root` can only be used when `--workspace` is also configured')
throw new Error('bad config')
}
}

const extRefFactory = new Factories.FromNodePackageJson.ExternalReferenceFactory()

myConsole.log('LOG | gathering BOM data ...')
Expand All @@ -257,7 +290,10 @@ export async function run (process: NodeJS.Process): Promise<number> {
gatherLicenseTexts: options.gatherLicenseTexts,
reproducible: options.outputReproducible,
flattenComponents: options.flattenComponents,
shortPURLs: options.shortPURLs
shortPURLs: options.shortPURLs,
workspace: options.workspace,
includeWorkspaceRoot: options.includeWorkspaceRoot,
workspaces: options.workspaces
},
myConsole
).buildFromProjectDir(projectDir, process)
Expand Down
26 changes: 25 additions & 1 deletion tests/integration/cli.args-pass-through.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,32 @@ describe('integration.cli.args-pass-through', () => {
['package-lock-only npm 8', `8.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm8ArgsGeneral, '--package-lock-only']],
['package-lock-only npm 9', `9.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm9ArgsGeneral, '--package-lock-only']],
['package-lock-only npm 10', `10.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm10ArgsGeneral, '--package-lock-only']],
['package-lock-only npm 11', `11.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm11ArgsGeneral, '--package-lock-only']]
['package-lock-only npm 11', `11.${rMinor}.${rPatch}`, ['--package-lock-only'], [...npm11ArgsGeneral, '--package-lock-only']],
// endregion package-lock-only
// region workspace
['workspace not supported npm 6', `6.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm6ArgsGeneral]],
['workspace npm 7', `7.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm7ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']],
['workspace npm 8', `8.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm8ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']],
['workspace npm 9', `9.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm9ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']],
['workspace npm 10', `10.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm10ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']],
['workspace npm 11', `11.${rMinor}.${rPatch}`, ['--workspace', 'my-wsA', '-w', 'my-wsB'], [...npm11ArgsGeneral, '--workspace=my-wsA', '--workspace=my-wsB']],
// endregion workspace
// region include-workspace-root
['workspace root not supported npm 6', `6.${rMinor}.${rPatch}`, ['-w', 'my-wsA', '--include-workspace-root'], [...npm6ArgsGeneral]],
['workspace root not supported npm 7', `7.${rMinor}.${rPatch}`, ['-w', 'my-wsA', '--include-workspace-root'], [...npm7ArgsGeneral, '--workspace=my-wsA']],
['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']],
['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']],
['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']],
['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']],
// endregion include-workspace-root
// region workspaces
['workspaces disabled not supported npm 6', `6.${rMinor}.${rPatch}`, ['--no-workspaces'], [...npm6ArgsGeneral]],
['workspaces disabled npm 7', `7.${rMinor}.${rPatch}`, ['--no-workspaces'], [...npm7ArgsGeneral, '--workspaces=false']],
['workspaces disabled npm 8', `8.${rMinor}.${rPatch}`, ['--no-workspaces'], [...npm8ArgsGeneral, '--workspaces=false']],
['workspaces disabled npm 9', `9.${rMinor}.${rPatch}`, ['--no-workspaces'], [...npm9ArgsGeneral, '--workspaces=false']],
['workspaces disabled npm 10', `10.${rMinor}.${rPatch}`, ['--no-workspaces'], [...npm10ArgsGeneral, '--workspaces=false']],
['workspaces disabled npm 11', `11.${rMinor}.${rPatch}`, ['--no-workspaces'], [...npm11ArgsGeneral, '--workspaces=false']]
// endregion workspaces
])('%s', async (purpose, npmVersion, cdxArgs, expectedArgs) => {
const logFileBase = join(tmpRootRun, purpose.replace(/\W/g, '_'))
const cwd = dummyProjectsRoot
Expand Down
Loading