Skip to content

Commit 50b3ab7

Browse files
jkowalleckmax619Copilot
authored
fix: prevent component duplication (#1456)
- Fixes #1418 - Supersedes #1421 --------- Signed-off-by: Maxim Bagryantsev <maxbag97619@gmail.com> Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com> Co-authored-by: Maxim Bagryantsev <maxbag97619@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 007af00 commit 50b3ab7

File tree

15 files changed

+6695
-152
lines changed

15 files changed

+6695
-152
lines changed

HISTORY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ All notable changes to this project will be documented in this file.
66

77
<!-- unreleased changes go here -->
88

9+
* Fixed
10+
* Prevent Component duplications ([#1418] via [#1456])
911
* Docs
1012
* Correct default value of option `specVersion` (via [#1460])
1113

14+
[#1418]: https://github.com/CycloneDX/cyclonedx-webpack-plugin/issues/1418
15+
[#1456]: https://github.com/CycloneDX/cyclonedx-webpack-plugin/pull/1456
1216
[#1460]: https://github.com/CycloneDX/cyclonedx-webpack-plugin/pull/1460
1317

1418
## 5.2.1 - 2025-11-05

src/_helpers.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@ export interface ValidPackageJSON {
4040
version: string
4141
}
4242

43-
export interface PackageDescription {
43+
export interface PackageDescription<PJ = any> {
4444
path: string
45-
packageJson: NonNullable<any>
45+
packageJson: NonNullable<PJ>
4646
}
4747

4848

4949
const PACKAGE_MANIFEST_FILENAME = 'package.json'
5050

51-
export function getPackageDescription(path: string): PackageDescription | undefined {
51+
export function getPackageDescription(path: string): PackageDescription<ValidPackageJSON> | undefined {
5252
const isSubDirOfNodeModules = isSubDirectoryOfNodeModulesFolder(path)
5353

5454
while (isAbsolute(path)) {
@@ -138,3 +138,4 @@ export function normalizePackageManifest (data: any, warn?: normalizePackageData
138138
data.version = oVersion.trim()
139139
}
140140
}
141+

src/extractor.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ export class Extractor {
5050
this.#leGatherer = leFetcher
5151
}
5252

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

5757
logger?.log('start building Components from modules...')
@@ -65,7 +65,7 @@ export class Extractor {
6565
logger?.debug('skipped package for', module.context)
6666
continue
6767
}
68-
let component = pkgs[pkg.path]
68+
let component = pkgs.get(pkg.path)
6969
if (component === undefined) {
7070
logger?.log('try to build new Component from PkgPath:', pkg.path)
7171
try {
@@ -76,7 +76,7 @@ export class Extractor {
7676
continue
7777
}
7878
logger?.debug('built', component, 'based on', pkg, 'for module', module)
79-
pkgs[pkg.path] = component
79+
pkgs.set(pkg.path, component)
8080
}
8181
components.set(module, component)
8282
}
@@ -85,7 +85,7 @@ export class Extractor {
8585
this.#linkDependencies(components)
8686

8787
logger?.log('done building Components from modules...')
88-
return components.values()
88+
return pkgs
8989
}
9090

9191
/**

src/plugin.ts

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,6 @@ export class CycloneDxWebpackPlugin {
206206

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

211210
const serializeOptions: CDX.Serialize.Types.SerializerOptions & CDX.Serialize.Types.NormalizerOptions = {
212211
sortLists: this.reproducibleResults,
@@ -247,6 +246,9 @@ export class CycloneDxWebpackPlugin {
247246
}
248247
}
249248

249+
const rcPath = getPackageDescription(compilation.compiler.context)?.path
250+
?? compilation.compiler.context
251+
250252
compilation.hooks.afterOptimizeTree.tap(
251253
pluginName,
252254
(_, modules) => {
@@ -259,23 +261,38 @@ export class CycloneDxWebpackPlugin {
259261
)
260262

261263
thisLogger.log('generating components...')
262-
for (const component of extractor.generateComponents(modules, this.collectEvidence, thisLogger.getChildLogger('Extractor'))) {
263-
if (bom.metadata.component !== undefined &&
264-
bom.metadata.component.group === component.group &&
265-
bom.metadata.component.name === component.name &&
266-
bom.metadata.component.version === component.version
267-
) {
268-
// metadata matches this exact component.
269-
// -> so the component is actually treated as the root component.
270-
thisLogger.debug('update bom.metadata.component - replace', bom.metadata.component, 'with', component)
271-
bom.metadata.component = component
264+
const components = extractor.generateComponents(modules, this.collectEvidence, thisLogger.getChildLogger('Extractor'))
265+
const rcComponentDetected = components.get(rcPath)
266+
if ( undefined !== rcComponentDetected ) {
267+
if (this.rootComponentAutodetect) {
268+
thisLogger.debug('set bom.metadata.component', rcComponentDetected)
269+
bom.metadata.component = rcComponentDetected
270+
components.delete(rcPath)
272271
} else {
272+
const rcComponent = cdxComponentBuilder.makeComponent({
273+
name: this.rootComponentName,
274+
version: this.rootComponentVersion,
275+
})
276+
if (rcComponent !== undefined) {
277+
rcComponent.dependencies = rcComponentDetected.dependencies
278+
for (const {dependencies} of components.values()) {
279+
if (dependencies.delete(rcComponentDetected.bomRef)) {
280+
dependencies.add(rcComponent.bomRef)
281+
}
282+
}
283+
thisLogger.debug('add to bom.metadata.component', rcComponentDetected)
284+
bom.metadata.component = rcComponent
285+
components.delete(rcPath)
286+
}
287+
}
288+
}
289+
for (const component of components.values()) {
273290
thisLogger.debug('add to bom.components', component)
274291
bom.components.add(component)
275-
}
276292
}
277293
thisLogger.log('generated components.')
278294

295+
279296
thisLogger.log('finalizing BOM...')
280297
this.#finalizeBom(bom, cdxComponentBuilder, cdxPurlFactory, logger.getChildLogger('BomFinalizer'))
281298
thisLogger.log('finalized BOM.')
@@ -368,25 +385,6 @@ export class CycloneDxWebpackPlugin {
368385
}
369386
}
370387

371-
#makeRootComponent (
372-
path: string,
373-
builder: CDX.Builders.FromNodePackageJson.ComponentBuilder,
374-
logger: WebpackLogger
375-
): CDX.Models.Component | undefined {
376-
/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- expected */
377-
const thisPackageJson = this.rootComponentAutodetect
378-
? getPackageDescription(path)?.packageJson
379-
: { name: this.rootComponentName, version: this.rootComponentVersion }
380-
if (thisPackageJson === undefined) { return undefined }
381-
normalizePackageManifest(
382-
383-
thisPackageJson,
384-
w => { logger.debug('normalizePackageJson from PkgPath', path, 'caused:', w) }
385-
)
386-
387-
return builder.makeComponent(thisPackageJson)
388-
}
389-
390388
#finalizeBom (
391389
bom: CDX.Models.Bom,
392390
cdxComponentBuilder: CDX.Builders.FromNodePackageJson.ComponentBuilder,
@@ -410,12 +408,13 @@ export class CycloneDxWebpackPlugin {
410408
bom.metadata.tools.components.add(toolC)
411409
}
412410

413-
if (bom.metadata.component !== undefined) {
414-
this.#addRootComponentExtRefs(bom.metadata.component, logger)
411+
const rComponent = bom.metadata.component
412+
if (rComponent !== undefined) {
413+
this.#addRootComponentExtRefs(rComponent, logger)
415414
/* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ack */
416-
bom.metadata.component.type = this.rootComponentType as CDX.Models.Component['type']
417-
bom.metadata.component.purl = cdxPurlFactory.makeFromComponent(bom.metadata.component)
418-
bom.metadata.component.bomRef.value = bom.metadata.component.purl?.toString()
415+
rComponent.type = this.rootComponentType as CDX.Models.Component['type']
416+
rComponent.purl = cdxPurlFactory.makeFromComponent(rComponent)
417+
rComponent.bomRef.value = rComponent.purl?.toString()
419418
}
420419
/* eslint-enable no-param-reassign */
421420
}

0 commit comments

Comments
 (0)