Skip to content
Merged
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
4 changes: 4 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ All notable changes to this project will be documented in this file.

<!-- unreleased changes go here -->

* Fixed
* Prevent Component duplications ([#1418] via [#1456])
* Docs
* Correct default value of option `specVersion` (via [#1460])

[#1418]: https://github.com/CycloneDX/cyclonedx-webpack-plugin/issues/1418
[#1456]: https://github.com/CycloneDX/cyclonedx-webpack-plugin/pull/1456
[#1460]: https://github.com/CycloneDX/cyclonedx-webpack-plugin/pull/1460

## 5.2.1 - 2025-11-05
Expand Down
7 changes: 4 additions & 3 deletions src/_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ export interface ValidPackageJSON {
version: string
}

export interface PackageDescription {
export interface PackageDescription<PJ = any> {
path: string
packageJson: NonNullable<any>
packageJson: NonNullable<PJ>
}


const PACKAGE_MANIFEST_FILENAME = 'package.json'

export function getPackageDescription(path: string): PackageDescription | undefined {
export function getPackageDescription(path: string): PackageDescription<ValidPackageJSON> | undefined {
const isSubDirOfNodeModules = isSubDirectoryOfNodeModulesFolder(path)

while (isAbsolute(path)) {
Expand Down Expand Up @@ -138,3 +138,4 @@ export function normalizePackageManifest (data: any, warn?: normalizePackageData
data.version = oVersion.trim()
}
}

10 changes: 5 additions & 5 deletions src/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export class Extractor {
this.#leGatherer = leFetcher
}

generateComponents (modules: Iterable<Module>, collectEvidence: boolean, logger?: WebpackLogger): Iterable<CDX.Models.Component> {
const pkgs: Record<string, CDX.Models.Component | undefined> = {}
generateComponents (modules: Iterable<Module>, collectEvidence: boolean, logger?: WebpackLogger): Map<string, CDX.Models.Component> {
const pkgs = new Map<string, CDX.Models.Component>()
const components = new Map<Module, CDX.Models.Component>()

logger?.log('start building Components from modules...')
Expand All @@ -65,7 +65,7 @@ export class Extractor {
logger?.debug('skipped package for', module.context)
continue
}
let component = pkgs[pkg.path]
let component = pkgs.get(pkg.path)
if (component === undefined) {
logger?.log('try to build new Component from PkgPath:', pkg.path)
try {
Expand All @@ -76,7 +76,7 @@ export class Extractor {
continue
}
logger?.debug('built', component, 'based on', pkg, 'for module', module)
pkgs[pkg.path] = component
pkgs.set(pkg.path, component)
}
components.set(module, component)
}
Expand All @@ -85,7 +85,7 @@ export class Extractor {
this.#linkDependencies(components)

logger?.log('done building Components from modules...')
return components.values()
return pkgs
}

/**
Expand Down
71 changes: 35 additions & 36 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ export class CycloneDxWebpackPlugin {

const bom = new CDX.Models.Bom()
bom.metadata.lifecycles.add(CDX.Enums.LifecyclePhase.Build)
bom.metadata.component = this.#makeRootComponent(compilation.compiler.context, cdxComponentBuilder, logger.getChildLogger('RootComponentBuilder'))

const serializeOptions: CDX.Serialize.Types.SerializerOptions & CDX.Serialize.Types.NormalizerOptions = {
sortLists: this.reproducibleResults,
Expand Down Expand Up @@ -247,6 +246,9 @@ export class CycloneDxWebpackPlugin {
}
}

const rcPath = getPackageDescription(compilation.compiler.context)?.path
?? compilation.compiler.context

compilation.hooks.afterOptimizeTree.tap(
pluginName,
(_, modules) => {
Expand All @@ -259,23 +261,38 @@ export class CycloneDxWebpackPlugin {
)

thisLogger.log('generating components...')
for (const component of extractor.generateComponents(modules, this.collectEvidence, thisLogger.getChildLogger('Extractor'))) {
if (bom.metadata.component !== undefined &&
bom.metadata.component.group === component.group &&
bom.metadata.component.name === component.name &&
bom.metadata.component.version === component.version
) {
// metadata matches this exact component.
// -> so the component is actually treated as the root component.
thisLogger.debug('update bom.metadata.component - replace', bom.metadata.component, 'with', component)
bom.metadata.component = component
const components = extractor.generateComponents(modules, this.collectEvidence, thisLogger.getChildLogger('Extractor'))
const rcComponentDetected = components.get(rcPath)
if ( undefined !== rcComponentDetected ) {
if (this.rootComponentAutodetect) {
thisLogger.debug('set bom.metadata.component', rcComponentDetected)
bom.metadata.component = rcComponentDetected
components.delete(rcPath)
} else {
const rcComponent = cdxComponentBuilder.makeComponent({
name: this.rootComponentName,
version: this.rootComponentVersion,
})
if (rcComponent !== undefined) {
rcComponent.dependencies = rcComponentDetected.dependencies
for (const {dependencies} of components.values()) {
if (dependencies.delete(rcComponentDetected.bomRef)) {
dependencies.add(rcComponent.bomRef)
}
}
thisLogger.debug('add to bom.metadata.component', rcComponentDetected)
bom.metadata.component = rcComponent
components.delete(rcPath)
}
}
}
for (const component of components.values()) {
thisLogger.debug('add to bom.components', component)
bom.components.add(component)
}
}
thisLogger.log('generated components.')


thisLogger.log('finalizing BOM...')
this.#finalizeBom(bom, cdxComponentBuilder, cdxPurlFactory, logger.getChildLogger('BomFinalizer'))
thisLogger.log('finalized BOM.')
Expand Down Expand Up @@ -368,25 +385,6 @@ export class CycloneDxWebpackPlugin {
}
}

#makeRootComponent (
path: string,
builder: CDX.Builders.FromNodePackageJson.ComponentBuilder,
logger: WebpackLogger
): CDX.Models.Component | undefined {
/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- expected */
const thisPackageJson = this.rootComponentAutodetect
? getPackageDescription(path)?.packageJson
: { name: this.rootComponentName, version: this.rootComponentVersion }
if (thisPackageJson === undefined) { return undefined }
normalizePackageManifest(

thisPackageJson,
w => { logger.debug('normalizePackageJson from PkgPath', path, 'caused:', w) }
)

return builder.makeComponent(thisPackageJson)
}

#finalizeBom (
bom: CDX.Models.Bom,
cdxComponentBuilder: CDX.Builders.FromNodePackageJson.ComponentBuilder,
Expand All @@ -410,12 +408,13 @@ export class CycloneDxWebpackPlugin {
bom.metadata.tools.components.add(toolC)
}

if (bom.metadata.component !== undefined) {
this.#addRootComponentExtRefs(bom.metadata.component, logger)
const rComponent = bom.metadata.component
if (rComponent !== undefined) {
this.#addRootComponentExtRefs(rComponent, logger)
/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ack */
bom.metadata.component.type = this.rootComponentType as CDX.Models.Component['type']
bom.metadata.component.purl = cdxPurlFactory.makeFromComponent(bom.metadata.component)
bom.metadata.component.bomRef.value = bom.metadata.component.purl?.toString()
rComponent.type = this.rootComponentType as CDX.Models.Component['type']
rComponent.purl = cdxPurlFactory.makeFromComponent(rComponent)
rComponent.bomRef.value = rComponent.purl?.toString()
}
/* eslint-enable no-param-reassign */
}
Expand Down
Loading