Skip to content

Commit

Permalink
feat: add support for targetting specific workspaces
Browse files Browse the repository at this point in the history
Signed-off-by: MalickBurger <[email protected]>
  • Loading branch information
MalickBurger committed Jan 17, 2025
1 parent aa98df3 commit 99114f6
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 40 deletions.
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')
}
}

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

0 comments on commit 99114f6

Please sign in to comment.