From 995a82b6d58ea7725fe6bae31b1a21d2490791aa Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 2 Nov 2026 12:03:28 +0100 Subject: [PATCH 01/14] feat: classes for optional features can indicate wehether they canact act Signed-off-by: Jan Kowalleck --- src/_optPlug.node/_wrapper.ts | 23 ++++++++++---- src/serialize/xmlSerializer.node.ts | 22 ++++++++------ src/validation/jsonValidator.node.ts | 30 +++++++++++-------- src/validation/xmlValidator.node.ts | 22 ++++++++------ .../Validation.JsonValidator.node.spec.js | 3 +- .../Validation.XmlValidator.node.spec.js | 3 +- .../Serialize.XmlSerialize.test.js | 7 ++--- .../Validation.JsonStrictValidator.test.js | 7 ++--- .../Validation.JsonValidator.test.js | 7 ++--- .../Validation.XmlValidator.test.js | 9 +++--- 10 files changed, 75 insertions(+), 58 deletions(-) diff --git a/src/_optPlug.node/_wrapper.ts b/src/_optPlug.node/_wrapper.ts index b34359269..ae81b680e 100644 --- a/src/_optPlug.node/_wrapper.ts +++ b/src/_optPlug.node/_wrapper.ts @@ -19,20 +19,31 @@ Copyright (c) OWASP Foundation. All Rights Reserved. import { OptPlugError } from './errors' -export type WillThrow = (() => never) & { fails: true } -function makeWIllThrow (message: string): WillThrow { +export type WillThrow = (() => never) & { fails: true, message: string } + +function makeWillThrow (message: string): WillThrow { const f: WillThrow = function (): never { - throw new OptPlugError(message) + throw new OptPlugError(f.message) } f.fails = true + f.message = message return Object.freeze(f) } -type PossibleFunctionalities = Array<[string, () => Functionality]> +/** @internal */ +export function isWillThrow(v: any): v is WillThrow { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ack */ + return typeof v === 'function' && (v as Partial).fails === true +} + + +type FunctionalityConstraint = CallableFunction & { fails?: false } + +type PossibleFunctionalities = Array<[string, () => Functionality]> /** @internal */ -export default function ( +export default function ( name: string, pf: PossibleFunctionalities ): Functionality | WillThrow { @@ -43,7 +54,7 @@ export default function ( /* pass */ } } - return makeWIllThrow( + return makeWillThrow( `No ${name} available.\n` + 'Please install one of the optional dependencies: ' + pf.map(kv => kv[0]).join(' || ') diff --git a/src/serialize/xmlSerializer.node.ts b/src/serialize/xmlSerializer.node.ts index bfc6ce056..75481ed3f 100644 --- a/src/serialize/xmlSerializer.node.ts +++ b/src/serialize/xmlSerializer.node.ts @@ -17,7 +17,7 @@ SPDX-License-Identifier: Apache-2.0 Copyright (c) OWASP Foundation. All Rights Reserved. */ -import { OptPlugError } from '../_optPlug.node/errors' +import { isWillThrow } from '../_optPlug.node/_wrapper' import stringify from '../_optPlug.node/xmlStringify' import { MissingOptionalDependencyError } from './errors' import type { SerializerOptions } from './types' @@ -30,6 +30,16 @@ import { XmlBaseSerializer } from './xmlBaseSerializer' export class XmlSerializer extends XmlBaseSerializer { // maybe override parent::serialize() and skip nonmalization and everything, in case `stringify.fails` is true + /** + * Indicates whether this functionality cannot act. + * + * `false` if this functionality can act; otherwise, describes which optional dependencies are + * unavailable and prevent it from doing so. + */ + static readonly cannot: false | Readonly<{message:string}> = isWillThrow(stringify) + ? Object.freeze({message: stringify.message}) + : false + /** * @throws {@link Serialize.MissingOptionalDependencyError | MissingOptionalDependencyError} * @throws {@link Error} @@ -38,13 +48,7 @@ export class XmlSerializer extends XmlBaseSerializer { normalizedBom: SimpleXml.Element, options: SerializerOptions = {} ): string { - try { - return stringify(normalizedBom, options) - } catch (err) { - if (err instanceof OptPlugError) { - throw new MissingOptionalDependencyError(err.message, err) - } - throw err - } + if (XmlSerializer.cannot !== false) throw new MissingOptionalDependencyError(XmlSerializer.cannot.message) + return stringify(normalizedBom, options) } } diff --git a/src/validation/jsonValidator.node.ts b/src/validation/jsonValidator.node.ts index 486a8082e..6629027b1 100644 --- a/src/validation/jsonValidator.node.ts +++ b/src/validation/jsonValidator.node.ts @@ -17,7 +17,7 @@ SPDX-License-Identifier: Apache-2.0 Copyright (c) OWASP Foundation. All Rights Reserved. */ -import { OptPlugError } from '../_optPlug.node/errors' +import { isWillThrow } from '../_optPlug.node/_wrapper' import makeValidator, { type Validator } from '../_optPlug.node/jsonValidator' import { FILES } from '../resources.node' import { BaseValidator } from './baseValidator' @@ -36,22 +36,26 @@ abstract class BaseJsonValidator extends BaseValidator { return s } + /** + * Indicates whether this functionality cannot act. + * + * `false` if this functionality can act; otherwise, describes which optional dependencies are + * unavailable and prevent it from doing so. + */ + static readonly cannot: false | Readonly<{message:string}> = isWillThrow(makeValidator) + ? Object.freeze({message: makeValidator.message}) + : false + #validatorCache?: Validator = undefined async #getValidator (): Promise { if (this.#validatorCache === undefined) { - try { - this.#validatorCache = await makeValidator(this.#getSchemaFilePath(), { - 'http://cyclonedx.org/schema/spdx.SNAPSHOT.schema.json': FILES.SPDX.JSON_SCHEMA, - 'http://cyclonedx.org/schema/cryptography-defs.SNAPSHOT.schema.json': FILES.CryptoDefs.JSON_SCHEMA, - 'http://cyclonedx.org/schema/jsf-0.82.SNAPSHOT.schema.json': FILES.JSF.JSON_SCHEMA - }) - } catch (err) { - if (err instanceof OptPlugError) { - throw new MissingOptionalDependencyError(err.message, err) - } - throw err - } + if (BaseJsonValidator.cannot !== false) throw new MissingOptionalDependencyError(BaseJsonValidator.cannot.message) + this.#validatorCache = await makeValidator(this.#getSchemaFilePath(), { + 'http://cyclonedx.org/schema/spdx.SNAPSHOT.schema.json': FILES.SPDX.JSON_SCHEMA, + 'http://cyclonedx.org/schema/cryptography-defs.SNAPSHOT.schema.json': FILES.CryptoDefs.JSON_SCHEMA, + 'http://cyclonedx.org/schema/jsf-0.82.SNAPSHOT.schema.json': FILES.JSF.JSON_SCHEMA + }) } return this.#validatorCache } diff --git a/src/validation/xmlValidator.node.ts b/src/validation/xmlValidator.node.ts index e007ee991..b39da597e 100644 --- a/src/validation/xmlValidator.node.ts +++ b/src/validation/xmlValidator.node.ts @@ -17,7 +17,7 @@ SPDX-License-Identifier: Apache-2.0 Copyright (c) OWASP Foundation. All Rights Reserved. */ -import { OptPlugError } from '../_optPlug.node/errors' +import { isWillThrow } from '../_optPlug.node/_wrapper' import makeValidator, { type Validator } from '../_optPlug.node/xmlValidator' import { FILES } from '../resources.node' import { BaseValidator } from './baseValidator' @@ -34,18 +34,22 @@ export class XmlValidator extends BaseValidator { return s } + /** + * Indicates whether this functionality cannot act. + * + * `false` if this functionality can act; otherwise, describes which optional dependencies are + * unavailable and prevent it from doing so. + */ + static readonly cannot: false | Readonly<{message:string}> = isWillThrow(makeValidator) + ? Object.freeze({message: makeValidator.message}) + : false + #validatorCache?: Validator = undefined async #getValidator (): Promise { if (this.#validatorCache === undefined) { - try { - this.#validatorCache = await makeValidator(this.#getSchemaFilePath()) - } catch (err) { - if (err instanceof OptPlugError) { - throw new MissingOptionalDependencyError(err.message, err) - } - throw err - } + if (XmlValidator.cannot !== false) throw new MissingOptionalDependencyError(XmlValidator.cannot.message) + this.#validatorCache = await makeValidator(this.#getSchemaFilePath()) } return this.#validatorCache } diff --git a/tests/functional/Validation.JsonValidator.node.spec.js b/tests/functional/Validation.JsonValidator.node.spec.js index 4122e49ac..aa0337ed4 100644 --- a/tests/functional/Validation.JsonValidator.node.spec.js +++ b/tests/functional/Validation.JsonValidator.node.spec.js @@ -30,8 +30,7 @@ const { } = require('../../') before(function () { - const { default: jsonValidator } = require('../../dist.node/_optPlug.node/jsonValidator') - if (jsonValidator.fails) { + if (JsonValidator.cannot !== false) { this.skip() } }) diff --git a/tests/functional/Validation.XmlValidator.node.spec.js b/tests/functional/Validation.XmlValidator.node.spec.js index 6e477b55f..f3ff4d59e 100644 --- a/tests/functional/Validation.XmlValidator.node.spec.js +++ b/tests/functional/Validation.XmlValidator.node.spec.js @@ -30,8 +30,7 @@ const { } = require('../../') before(function () { - const { default: xmlValidator } = require('../../dist.node/_optPlug.node/xmlValidator') - if (xmlValidator.fails) { + if (XmlValidator.cannot !== false) { this.skip() } }) diff --git a/tests/integration/Serialize.XmlSerialize.test.js b/tests/integration/Serialize.XmlSerialize.test.js index 562727a6e..91cb574ed 100644 --- a/tests/integration/Serialize.XmlSerialize.test.js +++ b/tests/integration/Serialize.XmlSerialize.test.js @@ -30,12 +30,11 @@ const { Spec, Validation } = require('../../') -const { default: xmlStringify } = require('../../dist.node/_optPlug.node/xmlStringify') const { createAllTools, createComplexStructure } = require('../_data/models') const { loadSerializeResult, writeSerializeResult } = require('../_data/serialize') describe('integration.Serialize.XmlSerialize', function () { - const expectMissingDepError = xmlStringify.fails ?? false + const expectMissingDepError = XmlSerializer.cannot this.timeout(60000) @@ -61,12 +60,12 @@ describe('integration.Serialize.XmlSerialize', function () { delete this.bom }) - if (expectMissingDepError) { + if (expectMissingDepError !== false) { it('throws MissingOptionalDependencyError', function () { const serializer = new XmlSerializer(normalizerFactory) assert.throws( () => { serializer.serialize(this.bom, {}) }, - (err) => err instanceof MissingOptionalDependencyError + (err) => err instanceof MissingOptionalDependencyError && err.message === expectMissingDepError.message ) }) return // skip other tests diff --git a/tests/integration/Validation.JsonStrictValidator.test.js b/tests/integration/Validation.JsonStrictValidator.test.js index 0dd80a394..0ea1deec2 100644 --- a/tests/integration/Validation.JsonStrictValidator.test.js +++ b/tests/integration/Validation.JsonStrictValidator.test.js @@ -28,10 +28,9 @@ const { JsonStrictValidator } } = require('../../') -const { default: jsonValidator } = require('../../dist.node/_optPlug.node/jsonValidator') describe('integration.Validation.JsonStrictValidator', () => { - const expectMissingDepError = jsonValidator.fails ?? false; + const expectMissingDepError = JsonStrictValidator.cannot; [ Version.v1dot0, @@ -54,12 +53,12 @@ describe('integration.Validation.JsonStrictValidator', () => { Version.v1dot3, Version.v1dot2 ].forEach((version) => describe(version, () => { - if (expectMissingDepError) { + if (expectMissingDepError !== false) { it('throws MissingOptionalDependencyError', async () => { const validator = new JsonStrictValidator(version) await assert.rejects( validator.validate('{}'), - (err) => err instanceof MissingOptionalDependencyError + (err) => err instanceof MissingOptionalDependencyError && err.message === expectMissingDepError.message ) }) return // skip other tests diff --git a/tests/integration/Validation.JsonValidator.test.js b/tests/integration/Validation.JsonValidator.test.js index 124db5b0f..aecbab71c 100644 --- a/tests/integration/Validation.JsonValidator.test.js +++ b/tests/integration/Validation.JsonValidator.test.js @@ -28,10 +28,9 @@ const { JsonValidator } } = require('../../') -const { default: jsonValidator } = require('../../dist.node/_optPlug.node/jsonValidator') describe('integration.Validation.JsonValidator', () => { - const expectMissingDepError = jsonValidator.fails ?? false; + const expectMissingDepError = JsonValidator.cannot; [ 'somthing-unexpected', @@ -55,12 +54,12 @@ describe('integration.Validation.JsonValidator', () => { Version.v1dot3, Version.v1dot2 ].forEach((version) => describe(version, () => { - if (expectMissingDepError) { + if (expectMissingDepError !== false) { it('throws MissingOptionalDependencyError', async () => { const validator = new JsonValidator(version) await assert.rejects( validator.validate('{}'), - (err) => err instanceof MissingOptionalDependencyError + (err) => err instanceof MissingOptionalDependencyError && err.message === expectMissingDepError.message ) }) return // skip other tests diff --git a/tests/integration/Validation.XmlValidator.test.js b/tests/integration/Validation.XmlValidator.test.js index f172fbc59..5e7ef0bd7 100644 --- a/tests/integration/Validation.XmlValidator.test.js +++ b/tests/integration/Validation.XmlValidator.test.js @@ -28,13 +28,12 @@ const { XmlValidator } } = require('../../') -const { default: xmlValidator } = require('../../dist.node/_optPlug.node/xmlValidator') describe('integration.Validation.XmlValidator', () => { - const expectMissingDepError = xmlValidator.fails ?? false; + const expectMissingDepError = XmlValidator.cannot; [ - 'somthing-unexpected' + 'something-unexpected' ].forEach((version) => describe(version, () => { it('throws not implemented', async () => { const validator = new XmlValidator(version) @@ -55,12 +54,12 @@ describe('integration.Validation.XmlValidator', () => { Version.v1dot1, Version.v1dot0 ].forEach((version) => describe(version, () => { - if (expectMissingDepError) { + if (expectMissingDepError !== false) { it('throws MissingOptionalDependencyError', async () => { const validator = new XmlValidator(version) await assert.rejects( validator.validate(''), - (err) => err instanceof MissingOptionalDependencyError + (err) => err instanceof MissingOptionalDependencyError && err.message === expectMissingDepError.message ) }) return // skip other tests From c22e0c42f37973697c9368780cf44e3750d9edd5 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 2 Nov 2026 12:09:51 +0100 Subject: [PATCH 02/14] docs Signed-off-by: Jan Kowalleck --- HISTORY.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 61b85a220..44c2f1832 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,17 @@ All notable changes to this project will be documented in this file. +* Added + * Optional-feature classes report when dependencies prevent them from acting (via [#1534]) + Adds static property `cannot`: `false` when available, otherwise an object with a `message1. + Affected classes: + * `Serialize.XmlSerializer` + * `Validation.JsonValidator` + * `Validation.JsonStrictValidator` + * `Validation.XmlValidator` + +[#1534]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1534 + ## 10.3.0 -- 2026-09-17 * Added From 17a96e3c35a7971201bd69dce1134577878d359e Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 2 Nov 2026 12:12:50 +0100 Subject: [PATCH 03/14] docs Signed-off-by: Jan Kowalleck --- HISTORY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 44c2f1832..571bd81a2 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -8,7 +8,7 @@ All notable changes to this project will be documented in this file. * Added * Optional-feature classes report when dependencies prevent them from acting (via [#1534]) - Adds static property `cannot`: `false` when available, otherwise an object with a `message1. + Adds a static property `cannot`: `false` when available, otherwise an object with a `message` detailing why it is unavailable. Affected classes: * `Serialize.XmlSerializer` * `Validation.JsonValidator` From a3d3432ba5f60d6ecf229ae1cfed49eb7e21a962 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 2 Nov 2026 13:14:03 +0100 Subject: [PATCH 04/14] tests Signed-off-by: Jan Kowalleck --- .../Serialize.XmlSerialize.test.js | 37 ++++++++++++------- .../Validation.JsonStrictValidator.test.js | 35 +++++++++++------- .../Validation.JsonValidator.test.js | 37 ++++++++++++------- .../Validation.XmlValidator.test.js | 35 +++++++++++------- 4 files changed, 91 insertions(+), 53 deletions(-) diff --git a/tests/integration/Serialize.XmlSerialize.test.js b/tests/integration/Serialize.XmlSerialize.test.js index 91cb574ed..82c224b29 100644 --- a/tests/integration/Serialize.XmlSerialize.test.js +++ b/tests/integration/Serialize.XmlSerialize.test.js @@ -19,7 +19,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved. const assert = require('node:assert') -const { afterEach, beforeEach, describe, it } = require('mocha') +const { afterEach, before, beforeEach, describe, it } = require('mocha') const { Models, Enums, @@ -34,7 +34,11 @@ const { createAllTools, createComplexStructure } = require('../_data/models') const { loadSerializeResult, writeSerializeResult } = require('../_data/serialize') describe('integration.Serialize.XmlSerialize', function () { - const expectMissingDepError = XmlSerializer.cannot + before(function () { + if (XmlNormalizeFactory.cannot !== false) { + this.skip() + } + }) this.timeout(60000) @@ -60,17 +64,6 @@ describe('integration.Serialize.XmlSerialize', function () { delete this.bom }) - if (expectMissingDepError !== false) { - it('throws MissingOptionalDependencyError', function () { - const serializer = new XmlSerializer(normalizerFactory) - assert.throws( - () => { serializer.serialize(this.bom, {}) }, - (err) => err instanceof MissingOptionalDependencyError && err.message === expectMissingDepError.message - ) - }) - return // skip other tests - } - it('serialize', async function () { const serializer = new XmlSerializer(normalizerFactory) const serialized = await serializer.serialize( @@ -159,3 +152,21 @@ describe('integration.Serialize.XmlSerialize', function () { }) }) }) + +describe('integration.Serialize.XmlSerialize cannot', function () { + before(function () { + if (XmlSerializer.cannot === false) { + this.skip() + } + }) + + it('deps missing throws MissingOptionalDependencyError', function () { + const normalizerFactory = new XmlNormalizeFactory(Spec.Spec1dot7) + const serializer = new XmlSerializer(normalizerFactory) + const bom = createComplexStructure() + assert.throws( + () => { serializer.serialize(bom, {}) }, + (err) => err instanceof MissingOptionalDependencyError && err.message === XmlSerializer.cannot.message + ) + }) +}) diff --git a/tests/integration/Validation.JsonStrictValidator.test.js b/tests/integration/Validation.JsonStrictValidator.test.js index 0ea1deec2..e952a54a4 100644 --- a/tests/integration/Validation.JsonStrictValidator.test.js +++ b/tests/integration/Validation.JsonStrictValidator.test.js @@ -19,7 +19,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved. const assert = require('node:assert') -const { describe, it } = require('mocha') +const { before, describe, it } = require('mocha') const { Spec: { Version }, @@ -30,7 +30,11 @@ const { } = require('../../') describe('integration.Validation.JsonStrictValidator', () => { - const expectMissingDepError = JsonStrictValidator.cannot; + before(function () { + if (JsonStrictValidator.cannot !== false) { + this.skip() + } + }); [ Version.v1dot0, @@ -53,17 +57,6 @@ describe('integration.Validation.JsonStrictValidator', () => { Version.v1dot3, Version.v1dot2 ].forEach((version) => describe(version, () => { - if (expectMissingDepError !== false) { - it('throws MissingOptionalDependencyError', async () => { - const validator = new JsonStrictValidator(version) - await assert.rejects( - validator.validate('{}'), - (err) => err instanceof MissingOptionalDependencyError && err.message === expectMissingDepError.message - ) - }) - return // skip other tests - } - it('invalid throws', async () => { const validator = new JsonStrictValidator(version) const input = JSON.stringify({ @@ -96,3 +89,19 @@ describe('integration.Validation.JsonStrictValidator', () => { }) })) }) + +describe('integration.Validation.JsonStrictValidator cannot', () => { + before(function () { + if (JsonStrictValidator.cannot === false) { + this.skip() + } + }) + + it('deps missing throws MissingOptionalDependencyError', async () => { + const validator = new JsonStrictValidator(Version.v1dot7) + await assert.rejects( + validator.validate('{}'), + (err) => err instanceof MissingOptionalDependencyError && err.message === JsonStrictValidator.cannot.message + ) + }) +}) diff --git a/tests/integration/Validation.JsonValidator.test.js b/tests/integration/Validation.JsonValidator.test.js index aecbab71c..a16bb8a2d 100644 --- a/tests/integration/Validation.JsonValidator.test.js +++ b/tests/integration/Validation.JsonValidator.test.js @@ -19,7 +19,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved. const assert = require('node:assert') -const { describe, it } = require('mocha') +const { before, describe, it } = require('mocha') const { Spec: { Version }, @@ -30,10 +30,14 @@ const { } = require('../../') describe('integration.Validation.JsonValidator', () => { - const expectMissingDepError = JsonValidator.cannot; + before(function () { + if (JsonValidator.cannot !== false) { + this.skip() + } + }); [ - 'somthing-unexpected', + 'something-unexpected', Version.v1dot0, Version.v1dot1 ].forEach((version) => describe(version, () => { @@ -54,17 +58,6 @@ describe('integration.Validation.JsonValidator', () => { Version.v1dot3, Version.v1dot2 ].forEach((version) => describe(version, () => { - if (expectMissingDepError !== false) { - it('throws MissingOptionalDependencyError', async () => { - const validator = new JsonValidator(version) - await assert.rejects( - validator.validate('{}'), - (err) => err instanceof MissingOptionalDependencyError && err.message === expectMissingDepError.message - ) - }) - return // skip other tests - } - it('invalid throws', async () => { const validator = new JsonValidator(version) const input = JSON.stringify({ @@ -97,3 +90,19 @@ describe('integration.Validation.JsonValidator', () => { }) })) }) + +describe('integration.Validation.JsonValidator cannot', () => { + before(function () { + if (JsonValidator.cannot === false) { + this.skip() + } + }) + + it('deps missing throws MissingOptionalDependencyError', async () => { + const validator = new JsonValidator(Version.Version.v1dot7) + await assert.rejects( + validator.validate('{}'), + (err) => err instanceof MissingOptionalDependencyError && err.message === JsonValidator.cannot.message + ) + }) +}) diff --git a/tests/integration/Validation.XmlValidator.test.js b/tests/integration/Validation.XmlValidator.test.js index 5e7ef0bd7..c5e9c38cf 100644 --- a/tests/integration/Validation.XmlValidator.test.js +++ b/tests/integration/Validation.XmlValidator.test.js @@ -19,7 +19,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved. const assert = require('node:assert') -const { describe, it } = require('mocha') +const { before, describe, it } = require('mocha') const { Spec: { Version }, @@ -30,7 +30,11 @@ const { } = require('../../') describe('integration.Validation.XmlValidator', () => { - const expectMissingDepError = XmlValidator.cannot; + before(function () { + if (XmlValidator.cannot !== false) { + this.skip() + } + }); [ 'something-unexpected' @@ -54,17 +58,6 @@ describe('integration.Validation.XmlValidator', () => { Version.v1dot1, Version.v1dot0 ].forEach((version) => describe(version, () => { - if (expectMissingDepError !== false) { - it('throws MissingOptionalDependencyError', async () => { - const validator = new XmlValidator(version) - await assert.rejects( - validator.validate(''), - (err) => err instanceof MissingOptionalDependencyError && err.message === expectMissingDepError.message - ) - }) - return // skip other tests - } - it('invalid throws', async () => { const validator = new XmlValidator(version) const input = ` @@ -98,3 +91,19 @@ describe('integration.Validation.XmlValidator', () => { }) })) }) + +describe('integration.Validation.XmlValidator cannot', () => { + before(function () { + if (XmlValidator.cannot === false) { + this.skip() + } + }) + + it('deps missing throws MissingOptionalDependencyError', async () => { + const validator = new XmlValidator(Version.v1dot7) + await assert.rejects( + validator.validate(''), + (err) => err instanceof MissingOptionalDependencyError && err.message === XmlValidator.cannot.message + ) + }) +}) From d24f51261ca8588054f45b85b80312477998a1a3 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 2 Nov 2026 13:35:06 +0100 Subject: [PATCH 05/14] interop Signed-off-by: Jan Kowalleck --- src/serialize/jsonSerializer.ts | 8 ++++++++ src/serialize/xmlSerializer.web.ts | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/serialize/jsonSerializer.ts b/src/serialize/jsonSerializer.ts index f85948d04..cef96f8f9 100644 --- a/src/serialize/jsonSerializer.ts +++ b/src/serialize/jsonSerializer.ts @@ -29,6 +29,14 @@ import type { NormalizerOptions, SerializerOptions } from './types' * Multi purpose Json serializer. */ export class JsonSerializer extends BaseSerializer { + /** + * Indicates whether this functionality cannot act. + * + * Always `false` for JSON functionality: no optional dependencies are involved, + * the platform-native `JSON.stringify` is used. + */ + static readonly cannot: false | Readonly<{ message: string }> = false + readonly #normalizerFactory: NormalizerFactory /** diff --git a/src/serialize/xmlSerializer.web.ts b/src/serialize/xmlSerializer.web.ts index 2671f501e..afc8c2471 100644 --- a/src/serialize/xmlSerializer.web.ts +++ b/src/serialize/xmlSerializer.web.ts @@ -26,6 +26,14 @@ import { XmlBaseSerializer } from './xmlBaseSerializer' * XML serializer for web browsers. */ export class XmlSerializer extends XmlBaseSerializer { + /** + * Indicates whether this functionality cannot act. + * + * Always `false` in web browsers: no optional dependencies are involved, + * the platform-native "XMLSerializer" is used. + */ + static readonly cannot: false | Readonly<{ message: string }> = false + protected _serialize ( normalizedBom: SimpleXml.Element, { space }: SerializerOptions = {} From dbd6b2561d4772e4961548ad71ce3aff8105b267 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 2 Nov 2026 14:41:03 +0100 Subject: [PATCH 06/14] wip Signed-off-by: Jan Kowalleck --- src/_opt.ts | 53 ++++++++++++++++++++++++++++ src/_optPlug.node/cannot | 1 + src/serialize/jsonSerializer.ts | 6 +++- src/serialize/xmlSerializer.node.ts | 13 ++++--- src/serialize/xmlSerializer.web.ts | 5 ++- src/validation/jsonValidator.node.ts | 26 ++++++++------ src/validation/xmlValidator.node.ts | 25 +++++++------ 7 files changed, 100 insertions(+), 29 deletions(-) create mode 100644 src/_opt.ts create mode 100644 src/_optPlug.node/cannot diff --git a/src/_opt.ts b/src/_opt.ts new file mode 100644 index 000000000..6fc74a950 --- /dev/null +++ b/src/_opt.ts @@ -0,0 +1,53 @@ +/*! +This file is part of CycloneDX JavaScript Library. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +Copyright (c) OWASP Foundation. All Rights Reserved. +*/ + +import { isWillThrow } from './_optPlug.node/_wrapper' + +/** + * Describes why an optional feature cannot act. + */ +interface Blocker { + readonly message: string +} + + +/** + * `false` if the optional feature can act; + * otherwise, a {@link Blocker} describing why the feature is unavailable. + */ +export type Cannot = false | Readonly + +/** + * Contract for classes that provide optional features. + * The class itself (not the instance) reports its availability. + */ +export interface Provider { + readonly cannot: Cannot +} + +/** + * Translate a pluggable functionality into an {@link Cannot}. + * + * @internal + */ +export function cannotOf (f: unknown): Cannot { + return isWillThrow(f) + ? Object.freeze({ message: f.message }) + : false +} diff --git a/src/_optPlug.node/cannot b/src/_optPlug.node/cannot new file mode 100644 index 000000000..8d070b27a --- /dev/null +++ b/src/_optPlug.node/cannot @@ -0,0 +1 @@ +cannotOf diff --git a/src/serialize/jsonSerializer.ts b/src/serialize/jsonSerializer.ts index cef96f8f9..162a5daa3 100644 --- a/src/serialize/jsonSerializer.ts +++ b/src/serialize/jsonSerializer.ts @@ -24,6 +24,8 @@ import { BaseSerializer } from './baseSerializer' import type { Factory as NormalizerFactory } from './json/normalize' import type { Normalized } from './json/types' import type { NormalizerOptions, SerializerOptions } from './types' +import type * as OptionalFeature from '../_opt' + /** * Multi purpose Json serializer. @@ -35,7 +37,7 @@ export class JsonSerializer extends BaseSerializer { * Always `false` for JSON functionality: no optional dependencies are involved, * the platform-native `JSON.stringify` is used. */ - static readonly cannot: false | Readonly<{ message: string }> = false + static readonly cannot: OptionalFeature.Cannot = false readonly #normalizerFactory: NormalizerFactory @@ -70,3 +72,5 @@ export class JsonSerializer extends BaseSerializer { return JSON.stringify(bom, null, space) } } + +JsonSerializer satisfies OptionalFeature.Provider diff --git a/src/serialize/xmlSerializer.node.ts b/src/serialize/xmlSerializer.node.ts index 75481ed3f..46622abce 100644 --- a/src/serialize/xmlSerializer.node.ts +++ b/src/serialize/xmlSerializer.node.ts @@ -17,18 +17,19 @@ SPDX-License-Identifier: Apache-2.0 Copyright (c) OWASP Foundation. All Rights Reserved. */ -import { isWillThrow } from '../_optPlug.node/_wrapper' import stringify from '../_optPlug.node/xmlStringify' import { MissingOptionalDependencyError } from './errors' import type { SerializerOptions } from './types' import type { SimpleXml } from './xml/types' import { XmlBaseSerializer } from './xmlBaseSerializer' +import { cannotOf } from '../_opt' +import type * as OptionalFeature from '../_opt' + /** * XML serializer for node. */ export class XmlSerializer extends XmlBaseSerializer { - // maybe override parent::serialize() and skip nonmalization and everything, in case `stringify.fails` is true /** * Indicates whether this functionality cannot act. @@ -36,9 +37,9 @@ export class XmlSerializer extends XmlBaseSerializer { * `false` if this functionality can act; otherwise, describes which optional dependencies are * unavailable and prevent it from doing so. */ - static readonly cannot: false | Readonly<{message:string}> = isWillThrow(stringify) - ? Object.freeze({message: stringify.message}) - : false + static readonly cannot: OptionalFeature.Cannot = cannotOf(stringify) + + // maybe override parent::serialize() and skip nonmalization and everything, in case `stringify.fails` is true /** * @throws {@link Serialize.MissingOptionalDependencyError | MissingOptionalDependencyError} @@ -52,3 +53,5 @@ export class XmlSerializer extends XmlBaseSerializer { return stringify(normalizedBom, options) } } + +XmlSerializer satisfies OptionalFeature.Provider diff --git a/src/serialize/xmlSerializer.web.ts b/src/serialize/xmlSerializer.web.ts index afc8c2471..d58da9a2c 100644 --- a/src/serialize/xmlSerializer.web.ts +++ b/src/serialize/xmlSerializer.web.ts @@ -21,6 +21,7 @@ import { isNotUndefined } from '../_helpers/notUndefined' import type { SerializerOptions } from './types' import type { SimpleXml } from './xml/types' import { XmlBaseSerializer } from './xmlBaseSerializer' +import type * as OptionalFeature from '../_opt' /** * XML serializer for web browsers. @@ -32,7 +33,7 @@ export class XmlSerializer extends XmlBaseSerializer { * Always `false` in web browsers: no optional dependencies are involved, * the platform-native "XMLSerializer" is used. */ - static readonly cannot: false | Readonly<{ message: string }> = false + static readonly cannot: OptionalFeature.Cannot = false protected _serialize ( normalizedBom: SimpleXml.Element, @@ -99,3 +100,5 @@ export class XmlSerializer extends XmlBaseSerializer { } } } + +XmlSerializer satisfies OptionalFeature.Provider diff --git a/src/validation/jsonValidator.node.ts b/src/validation/jsonValidator.node.ts index 6629027b1..f6cec7db2 100644 --- a/src/validation/jsonValidator.node.ts +++ b/src/validation/jsonValidator.node.ts @@ -17,14 +17,25 @@ SPDX-License-Identifier: Apache-2.0 Copyright (c) OWASP Foundation. All Rights Reserved. */ -import { isWillThrow } from '../_optPlug.node/_wrapper' import makeValidator, { type Validator } from '../_optPlug.node/jsonValidator' import { FILES } from '../resources.node' import { BaseValidator } from './baseValidator' import { MissingOptionalDependencyError, NotImplementedError } from './errors' import type { ValidationError } from './types' +import type * as OptionalFeature from '../_opt' +import { cannotOf } from '../_opt' + abstract class BaseJsonValidator extends BaseValidator { + + /** + * Indicates whether this functionality cannot act. + * + * `false` if this functionality can act; otherwise, describes which optional dependencies are + * unavailable and prevent it from doing so. + */ + static readonly cannot = cannotOf(makeValidator) + /** @internal */ protected abstract _getSchemaFile (): string | undefined @@ -36,16 +47,6 @@ abstract class BaseJsonValidator extends BaseValidator { return s } - /** - * Indicates whether this functionality cannot act. - * - * `false` if this functionality can act; otherwise, describes which optional dependencies are - * unavailable and prevent it from doing so. - */ - static readonly cannot: false | Readonly<{message:string}> = isWillThrow(makeValidator) - ? Object.freeze({message: makeValidator.message}) - : false - #validatorCache?: Validator = undefined async #getValidator (): Promise { @@ -75,6 +76,9 @@ abstract class BaseJsonValidator extends BaseValidator { return (await this.#getValidator())(data) } } + +BaseJsonValidator satisfies OptionalFeature.Provider + export class JsonValidator extends BaseJsonValidator { /** @internal */ protected _getSchemaFile (): string | undefined { diff --git a/src/validation/xmlValidator.node.ts b/src/validation/xmlValidator.node.ts index b39da597e..a736fc2e6 100644 --- a/src/validation/xmlValidator.node.ts +++ b/src/validation/xmlValidator.node.ts @@ -17,14 +17,25 @@ SPDX-License-Identifier: Apache-2.0 Copyright (c) OWASP Foundation. All Rights Reserved. */ -import { isWillThrow } from '../_optPlug.node/_wrapper' import makeValidator, { type Validator } from '../_optPlug.node/xmlValidator' import { FILES } from '../resources.node' import { BaseValidator } from './baseValidator' import { MissingOptionalDependencyError, NotImplementedError } from './errors' import type { ValidationError } from './types' +import type * as OptionalFeature from '../_opt' +import { cannotOf } from '../_opt' + export class XmlValidator extends BaseValidator { + + /** + * Indicates whether this functionality cannot act. + * + * `false` if this functionality can act; otherwise, describes which optional dependencies are + * unavailable and prevent it from doing so. + */ + static readonly cannot = cannotOf(makeValidator) + #getSchemaFilePath (): string { const s = FILES.CDX.XML_SCHEMA[this.version] /* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- better be safe */ @@ -34,16 +45,6 @@ export class XmlValidator extends BaseValidator { return s } - /** - * Indicates whether this functionality cannot act. - * - * `false` if this functionality can act; otherwise, describes which optional dependencies are - * unavailable and prevent it from doing so. - */ - static readonly cannot: false | Readonly<{message:string}> = isWillThrow(makeValidator) - ? Object.freeze({message: makeValidator.message}) - : false - #validatorCache?: Validator = undefined async #getValidator (): Promise { @@ -69,3 +70,5 @@ export class XmlValidator extends BaseValidator { return (await this.#getValidator())(data) } } + +XmlValidator satisfies OptionalFeature.Provider From abf6b203fa50fdc96c96598f06a5bd378b42b5cb Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 2 Nov 2026 14:42:14 +0100 Subject: [PATCH 07/14] wip Signed-off-by: Jan Kowalleck --- src/serialize/jsonSerializer.ts | 2 +- src/serialize/xmlSerializer.node.ts | 4 ++-- src/serialize/xmlSerializer.web.ts | 2 +- src/validation/jsonValidator.node.ts | 4 ++-- src/validation/xmlValidator.node.ts | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/serialize/jsonSerializer.ts b/src/serialize/jsonSerializer.ts index 162a5daa3..27389fe66 100644 --- a/src/serialize/jsonSerializer.ts +++ b/src/serialize/jsonSerializer.ts @@ -17,6 +17,7 @@ SPDX-License-Identifier: Apache-2.0 Copyright (c) OWASP Foundation. All Rights Reserved. */ +import type * as OptionalFeature from '../_opt' import type { Bom } from '../models/bom' import { Format } from '../spec/enums' import { UnsupportedFormatError } from '../spec/errors' @@ -24,7 +25,6 @@ import { BaseSerializer } from './baseSerializer' import type { Factory as NormalizerFactory } from './json/normalize' import type { Normalized } from './json/types' import type { NormalizerOptions, SerializerOptions } from './types' -import type * as OptionalFeature from '../_opt' /** diff --git a/src/serialize/xmlSerializer.node.ts b/src/serialize/xmlSerializer.node.ts index 46622abce..dec827a1c 100644 --- a/src/serialize/xmlSerializer.node.ts +++ b/src/serialize/xmlSerializer.node.ts @@ -17,13 +17,13 @@ SPDX-License-Identifier: Apache-2.0 Copyright (c) OWASP Foundation. All Rights Reserved. */ +import type * as OptionalFeature from '../_opt' +import { cannotOf } from '../_opt' import stringify from '../_optPlug.node/xmlStringify' import { MissingOptionalDependencyError } from './errors' import type { SerializerOptions } from './types' import type { SimpleXml } from './xml/types' import { XmlBaseSerializer } from './xmlBaseSerializer' -import { cannotOf } from '../_opt' -import type * as OptionalFeature from '../_opt' /** diff --git a/src/serialize/xmlSerializer.web.ts b/src/serialize/xmlSerializer.web.ts index d58da9a2c..b0f16bccf 100644 --- a/src/serialize/xmlSerializer.web.ts +++ b/src/serialize/xmlSerializer.web.ts @@ -18,10 +18,10 @@ Copyright (c) OWASP Foundation. All Rights Reserved. */ import { isNotUndefined } from '../_helpers/notUndefined' +import type * as OptionalFeature from '../_opt' import type { SerializerOptions } from './types' import type { SimpleXml } from './xml/types' import { XmlBaseSerializer } from './xmlBaseSerializer' -import type * as OptionalFeature from '../_opt' /** * XML serializer for web browsers. diff --git a/src/validation/jsonValidator.node.ts b/src/validation/jsonValidator.node.ts index f6cec7db2..097579e77 100644 --- a/src/validation/jsonValidator.node.ts +++ b/src/validation/jsonValidator.node.ts @@ -17,13 +17,13 @@ SPDX-License-Identifier: Apache-2.0 Copyright (c) OWASP Foundation. All Rights Reserved. */ +import type * as OptionalFeature from '../_opt' +import { cannotOf } from '../_opt' import makeValidator, { type Validator } from '../_optPlug.node/jsonValidator' import { FILES } from '../resources.node' import { BaseValidator } from './baseValidator' import { MissingOptionalDependencyError, NotImplementedError } from './errors' import type { ValidationError } from './types' -import type * as OptionalFeature from '../_opt' -import { cannotOf } from '../_opt' abstract class BaseJsonValidator extends BaseValidator { diff --git a/src/validation/xmlValidator.node.ts b/src/validation/xmlValidator.node.ts index a736fc2e6..444fefbb0 100644 --- a/src/validation/xmlValidator.node.ts +++ b/src/validation/xmlValidator.node.ts @@ -17,13 +17,13 @@ SPDX-License-Identifier: Apache-2.0 Copyright (c) OWASP Foundation. All Rights Reserved. */ +import type * as OptionalFeature from '../_opt' +import { cannotOf } from '../_opt' import makeValidator, { type Validator } from '../_optPlug.node/xmlValidator' import { FILES } from '../resources.node' import { BaseValidator } from './baseValidator' import { MissingOptionalDependencyError, NotImplementedError } from './errors' import type { ValidationError } from './types' -import type * as OptionalFeature from '../_opt' -import { cannotOf } from '../_opt' export class XmlValidator extends BaseValidator { From 4da7d5a34e95c7da81767fc623978b4e2b0b8b62 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 2 Nov 2026 14:49:47 +0100 Subject: [PATCH 08/14] wip Signed-off-by: Jan Kowalleck --- .github/workflows/nodejs.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 7896f46da..047ddf105 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -245,9 +245,7 @@ jobs: name: dist.node path: dist.node - name: test - run: > - npm run -- test:node - ${{ startsWith(matrix.node-version, env.NODE_ACTIVE_LTS) && ' --forbid-pending' || '' }} + run: npm run -- test:node - name: collect coverage if: ${{ failure() || success() }} run: > From 66c908dec230cdf3e854c15138225998811dcfba Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 2 Nov 2026 15:00:08 +0100 Subject: [PATCH 09/14] wip Signed-off-by: Jan Kowalleck --- src/_opt.ts | 13 -------- src/_optPlug.node/cannot | 1 - src/_optPlug.node/cannot.ts | 32 +++++++++++++++++++ src/serialize/xmlSerializer.node.ts | 2 +- src/validation/jsonValidator.node.ts | 2 +- src/validation/xmlValidator.node.ts | 2 +- .../Serialize.XmlSerialize.test.js | 2 +- .../Validation.JsonValidator.test.js | 2 +- 8 files changed, 37 insertions(+), 19 deletions(-) delete mode 100644 src/_optPlug.node/cannot create mode 100644 src/_optPlug.node/cannot.ts diff --git a/src/_opt.ts b/src/_opt.ts index 6fc74a950..84957f1bd 100644 --- a/src/_opt.ts +++ b/src/_opt.ts @@ -17,8 +17,6 @@ SPDX-License-Identifier: Apache-2.0 Copyright (c) OWASP Foundation. All Rights Reserved. */ -import { isWillThrow } from './_optPlug.node/_wrapper' - /** * Describes why an optional feature cannot act. */ @@ -40,14 +38,3 @@ export type Cannot = false | Readonly export interface Provider { readonly cannot: Cannot } - -/** - * Translate a pluggable functionality into an {@link Cannot}. - * - * @internal - */ -export function cannotOf (f: unknown): Cannot { - return isWillThrow(f) - ? Object.freeze({ message: f.message }) - : false -} diff --git a/src/_optPlug.node/cannot b/src/_optPlug.node/cannot deleted file mode 100644 index 8d070b27a..000000000 --- a/src/_optPlug.node/cannot +++ /dev/null @@ -1 +0,0 @@ -cannotOf diff --git a/src/_optPlug.node/cannot.ts b/src/_optPlug.node/cannot.ts new file mode 100644 index 000000000..d920f518e --- /dev/null +++ b/src/_optPlug.node/cannot.ts @@ -0,0 +1,32 @@ +/*! +This file is part of CycloneDX JavaScript Library. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +Copyright (c) OWASP Foundation. All Rights Reserved. +*/ + +import type { Cannot } from '../_opt' +import { isWillThrow } from './_wrapper' + +/** + * Translate a pluggable functionality into an {@link Cannot}. + * + * @internal + */ +export function cannotOf (f: unknown): Cannot { + return isWillThrow(f) + ? Object.freeze({ message: f.message }) + : false +} diff --git a/src/serialize/xmlSerializer.node.ts b/src/serialize/xmlSerializer.node.ts index dec827a1c..70fbd9dd9 100644 --- a/src/serialize/xmlSerializer.node.ts +++ b/src/serialize/xmlSerializer.node.ts @@ -18,7 +18,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved. */ import type * as OptionalFeature from '../_opt' -import { cannotOf } from '../_opt' +import { cannotOf } from '../_optPlug.node/cannot' import stringify from '../_optPlug.node/xmlStringify' import { MissingOptionalDependencyError } from './errors' import type { SerializerOptions } from './types' diff --git a/src/validation/jsonValidator.node.ts b/src/validation/jsonValidator.node.ts index 097579e77..eeeddc9c8 100644 --- a/src/validation/jsonValidator.node.ts +++ b/src/validation/jsonValidator.node.ts @@ -18,7 +18,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved. */ import type * as OptionalFeature from '../_opt' -import { cannotOf } from '../_opt' +import { cannotOf } from '../_optPlug.node/cannot' import makeValidator, { type Validator } from '../_optPlug.node/jsonValidator' import { FILES } from '../resources.node' import { BaseValidator } from './baseValidator' diff --git a/src/validation/xmlValidator.node.ts b/src/validation/xmlValidator.node.ts index 444fefbb0..616b2431e 100644 --- a/src/validation/xmlValidator.node.ts +++ b/src/validation/xmlValidator.node.ts @@ -18,7 +18,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved. */ import type * as OptionalFeature from '../_opt' -import { cannotOf } from '../_opt' +import { cannotOf } from '../_optPlug.node/cannot' import makeValidator, { type Validator } from '../_optPlug.node/xmlValidator' import { FILES } from '../resources.node' import { BaseValidator } from './baseValidator' diff --git a/tests/integration/Serialize.XmlSerialize.test.js b/tests/integration/Serialize.XmlSerialize.test.js index 82c224b29..c87fcba80 100644 --- a/tests/integration/Serialize.XmlSerialize.test.js +++ b/tests/integration/Serialize.XmlSerialize.test.js @@ -35,7 +35,7 @@ const { loadSerializeResult, writeSerializeResult } = require('../_data/serializ describe('integration.Serialize.XmlSerialize', function () { before(function () { - if (XmlNormalizeFactory.cannot !== false) { + if (XmlSerializer.cannot !== false) { this.skip() } }) diff --git a/tests/integration/Validation.JsonValidator.test.js b/tests/integration/Validation.JsonValidator.test.js index a16bb8a2d..2de450660 100644 --- a/tests/integration/Validation.JsonValidator.test.js +++ b/tests/integration/Validation.JsonValidator.test.js @@ -99,7 +99,7 @@ describe('integration.Validation.JsonValidator cannot', () => { }) it('deps missing throws MissingOptionalDependencyError', async () => { - const validator = new JsonValidator(Version.Version.v1dot7) + const validator = new JsonValidator(Version.v1dot7) await assert.rejects( validator.validate('{}'), (err) => err instanceof MissingOptionalDependencyError && err.message === JsonValidator.cannot.message From e2327ad87dd5fa9300ef2e7377ff0dcfa9857672 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 2 Nov 2026 15:02:26 +0100 Subject: [PATCH 10/14] wip Signed-off-by: Jan Kowalleck --- src/_optPlug.node/cannot.ts | 2 +- src/serialize/xmlSerializer.node.ts | 2 +- src/validation/jsonValidator.node.ts | 2 +- src/validation/xmlValidator.node.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_optPlug.node/cannot.ts b/src/_optPlug.node/cannot.ts index d920f518e..04251ded4 100644 --- a/src/_optPlug.node/cannot.ts +++ b/src/_optPlug.node/cannot.ts @@ -25,7 +25,7 @@ import { isWillThrow } from './_wrapper' * * @internal */ -export function cannotOf (f: unknown): Cannot { +export default function cannotOf (f: unknown): Cannot { return isWillThrow(f) ? Object.freeze({ message: f.message }) : false diff --git a/src/serialize/xmlSerializer.node.ts b/src/serialize/xmlSerializer.node.ts index 70fbd9dd9..0790994d6 100644 --- a/src/serialize/xmlSerializer.node.ts +++ b/src/serialize/xmlSerializer.node.ts @@ -18,7 +18,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved. */ import type * as OptionalFeature from '../_opt' -import { cannotOf } from '../_optPlug.node/cannot' +import cannotOf from '../_optPlug.node/cannot' import stringify from '../_optPlug.node/xmlStringify' import { MissingOptionalDependencyError } from './errors' import type { SerializerOptions } from './types' diff --git a/src/validation/jsonValidator.node.ts b/src/validation/jsonValidator.node.ts index eeeddc9c8..45162093b 100644 --- a/src/validation/jsonValidator.node.ts +++ b/src/validation/jsonValidator.node.ts @@ -18,7 +18,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved. */ import type * as OptionalFeature from '../_opt' -import { cannotOf } from '../_optPlug.node/cannot' +import cannotOf from '../_optPlug.node/cannot' import makeValidator, { type Validator } from '../_optPlug.node/jsonValidator' import { FILES } from '../resources.node' import { BaseValidator } from './baseValidator' diff --git a/src/validation/xmlValidator.node.ts b/src/validation/xmlValidator.node.ts index 616b2431e..134e17387 100644 --- a/src/validation/xmlValidator.node.ts +++ b/src/validation/xmlValidator.node.ts @@ -18,7 +18,7 @@ Copyright (c) OWASP Foundation. All Rights Reserved. */ import type * as OptionalFeature from '../_opt' -import { cannotOf } from '../_optPlug.node/cannot' +import cannotOf from '../_optPlug.node/cannot' import makeValidator, { type Validator } from '../_optPlug.node/xmlValidator' import { FILES } from '../resources.node' import { BaseValidator } from './baseValidator' From c414d9e09be297eb46721cf000e6d0a233ababad Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 2 Nov 2026 15:06:28 +0100 Subject: [PATCH 11/14] wip Signed-off-by: Jan Kowalleck --- src/_optPlug.node/_wrapper.ts | 2 +- src/serialize/xmlSerializer.node.ts | 2 +- src/validation/jsonValidator.node.ts | 2 +- src/validation/xmlValidator.node.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_optPlug.node/_wrapper.ts b/src/_optPlug.node/_wrapper.ts index ae81b680e..b380cb69b 100644 --- a/src/_optPlug.node/_wrapper.ts +++ b/src/_optPlug.node/_wrapper.ts @@ -32,7 +32,7 @@ function makeWillThrow (message: string): WillThrow { } /** @internal */ -export function isWillThrow(v: any): v is WillThrow { +export function isWillThrow (v: any): v is WillThrow { /* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ack */ return typeof v === 'function' && (v as Partial).fails === true } diff --git a/src/serialize/xmlSerializer.node.ts b/src/serialize/xmlSerializer.node.ts index 0790994d6..283ac0909 100644 --- a/src/serialize/xmlSerializer.node.ts +++ b/src/serialize/xmlSerializer.node.ts @@ -37,7 +37,7 @@ export class XmlSerializer extends XmlBaseSerializer { * `false` if this functionality can act; otherwise, describes which optional dependencies are * unavailable and prevent it from doing so. */ - static readonly cannot: OptionalFeature.Cannot = cannotOf(stringify) + static readonly cannot: OptionalFeature.Cannot = cannotOf(stringify) // maybe override parent::serialize() and skip nonmalization and everything, in case `stringify.fails` is true diff --git a/src/validation/jsonValidator.node.ts b/src/validation/jsonValidator.node.ts index 45162093b..157c585d5 100644 --- a/src/validation/jsonValidator.node.ts +++ b/src/validation/jsonValidator.node.ts @@ -34,7 +34,7 @@ abstract class BaseJsonValidator extends BaseValidator { * `false` if this functionality can act; otherwise, describes which optional dependencies are * unavailable and prevent it from doing so. */ - static readonly cannot = cannotOf(makeValidator) + static readonly cannot: OptionalFeature.Cannot = cannotOf(makeValidator) /** @internal */ protected abstract _getSchemaFile (): string | undefined diff --git a/src/validation/xmlValidator.node.ts b/src/validation/xmlValidator.node.ts index 134e17387..9de2ef314 100644 --- a/src/validation/xmlValidator.node.ts +++ b/src/validation/xmlValidator.node.ts @@ -34,7 +34,7 @@ export class XmlValidator extends BaseValidator { * `false` if this functionality can act; otherwise, describes which optional dependencies are * unavailable and prevent it from doing so. */ - static readonly cannot = cannotOf(makeValidator) + static readonly cannot: OptionalFeature.Cannot = cannotOf(makeValidator) #getSchemaFilePath (): string { const s = FILES.CDX.XML_SCHEMA[this.version] From ef681185a514caeb2d9dbc52da611fcbf3ed0fa0 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 2 Nov 2026 15:08:46 +0100 Subject: [PATCH 12/14] wip Signed-off-by: Jan Kowalleck --- src/serialize/xmlSerializer.node.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/serialize/xmlSerializer.node.ts b/src/serialize/xmlSerializer.node.ts index 283ac0909..11bf46a0d 100644 --- a/src/serialize/xmlSerializer.node.ts +++ b/src/serialize/xmlSerializer.node.ts @@ -39,8 +39,6 @@ export class XmlSerializer extends XmlBaseSerializer { */ static readonly cannot: OptionalFeature.Cannot = cannotOf(stringify) - // maybe override parent::serialize() and skip nonmalization and everything, in case `stringify.fails` is true - /** * @throws {@link Serialize.MissingOptionalDependencyError | MissingOptionalDependencyError} * @throws {@link Error} From 0841ce763b7c2995cae082e85a057e0ea7bcef6e Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 2 Nov 2026 15:10:28 +0100 Subject: [PATCH 13/14] wip Signed-off-by: Jan Kowalleck --- HISTORY.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 571bd81a2..021d1a5b5 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -7,13 +7,20 @@ All notable changes to this project will be documented in this file. * Added - * Optional-feature classes report when dependencies prevent them from acting (via [#1534]) - Adds a static property `cannot`: `false` when available, otherwise an object with a `message` detailing why it is unavailable. + * Optional-feature classes can report whether they are unable to act, + due to missing optional dependencies (via [#1534]) + They gained a static readonly property `cannot`, which is `false` when the feature is available, + otherwise an object with a `message` describing why it is unavailable. Affected classes: - * `Serialize.XmlSerializer` + * `Serialize.JsonSerializer` — always `false` + * `Serialize.XmlSerializer` — always `false` in web builds; in node depends on an optional XML library * `Validation.JsonValidator` * `Validation.JsonStrictValidator` * `Validation.XmlValidator` + * Changed + * `Serialize.XmlSerializer`, `Validation.JsonValidator`, `Validation.JsonStrictValidator`, + `Validation.XmlValidator` now throw `MissingOptionalDependencyError` eagerly via the `cannot` check, + instead of wrapping the underlying `OptPlugError` (via [#1534]) [#1534]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1534 From 617a99fd16cee94d53338078fd379445f0fe1131 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 2 Nov 2026 15:13:47 +0100 Subject: [PATCH 14/14] wip Signed-off-by: Jan Kowalleck --- HISTORY.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 021d1a5b5..501f206a0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -7,20 +7,20 @@ All notable changes to this project will be documented in this file. * Added - * Optional-feature classes can report whether they are unable to act, - due to missing optional dependencies (via [#1534]) - They gained a static readonly property `cannot`, which is `false` when the feature is available, - otherwise an object with a `message` describing why it is unavailable. + * Optional-feature classes expose a static `cannot` property (via [#1534]) + It tells whether missing optional dependencies prevent the class from acting: + `false` when the feature is available; + otherwise an object with a `message` describing why the feature is unavailable. Affected classes: * `Serialize.JsonSerializer` — always `false` - * `Serialize.XmlSerializer` — always `false` in web builds; in node depends on an optional XML library + * `Serialize.XmlSerializer` — always `false` in web builds; in node it depends on an optional XML library * `Validation.JsonValidator` * `Validation.JsonStrictValidator` * `Validation.XmlValidator` - * Changed - * `Serialize.XmlSerializer`, `Validation.JsonValidator`, `Validation.JsonStrictValidator`, - `Validation.XmlValidator` now throw `MissingOptionalDependencyError` eagerly via the `cannot` check, - instead of wrapping the underlying `OptPlugError` (via [#1534]) +* Changed + * `Serialize.XmlSerializer`, `Validation.JsonValidator`, `Validation.JsonStrictValidator` and `Validation.XmlValidator` + now throw `MissingOptionalDependencyError` based on the new `cannot` check, + instead of wrapping the underlying `OptPlugError` (via [#1534]) [#1534]: https://github.com/CycloneDX/cyclonedx-javascript-library/pull/1534