Skip to content
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
4 changes: 1 addition & 3 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: >
Expand Down
18 changes: 18 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ All notable changes to this project will be documented in this file.

<!-- add unreleased items here -->

* Added
* 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 it depends on an optional XML library
* `Validation.JsonValidator`
* `Validation.JsonStrictValidator`
* `Validation.XmlValidator`
* 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

## 10.3.0 -- 2026-09-17

* Added
Expand Down
40 changes: 40 additions & 0 deletions src/_opt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*!
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.
*/

/**
* 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<Blocker>

/**
* Contract for classes that provide optional features.
* The class itself (not the instance) reports its availability.
*/
export interface Provider {
readonly cannot: Cannot
}
23 changes: 17 additions & 6 deletions src/_optPlug.node/_wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Functionality> = 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<WillThrow>).fails === true
}


type FunctionalityConstraint = CallableFunction & { fails?: false }

type PossibleFunctionalities<Functionality extends FunctionalityConstraint> = Array<[string, () => Functionality]>

/** @internal */
export default function <Functionality> (
export default function <Functionality extends FunctionalityConstraint>(
name: string,
pf: PossibleFunctionalities<Functionality>
): Functionality | WillThrow {
Expand All @@ -43,7 +54,7 @@ export default function <Functionality> (
/* pass */
}
}
return makeWIllThrow(
return makeWillThrow(
`No ${name} available.\n` +
'Please install one of the optional dependencies: ' +
pf.map(kv => kv[0]).join(' || ')
Expand Down
32 changes: 32 additions & 0 deletions src/_optPlug.node/cannot.ts
Original file line number Diff line number Diff line change
@@ -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 default function cannotOf (f: unknown): Cannot {
return isWillThrow(f)
? Object.freeze({ message: f.message })
: false
}
12 changes: 12 additions & 0 deletions src/serialize/jsonSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -25,10 +26,19 @@ import type { Factory as NormalizerFactory } from './json/normalize'
import type { Normalized } from './json/types'
import type { NormalizerOptions, SerializerOptions } from './types'


/**
* Multi purpose Json serializer.
*/
export class JsonSerializer extends BaseSerializer<Normalized.Bom> {
/**
* 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: OptionalFeature.Cannot = false

readonly #normalizerFactory: NormalizerFactory

/**
Expand Down Expand Up @@ -62,3 +72,5 @@ export class JsonSerializer extends BaseSerializer<Normalized.Bom> {
return JSON.stringify(bom, null, space)
}
}

JsonSerializer satisfies OptionalFeature.Provider
25 changes: 15 additions & 10 deletions src/serialize/xmlSerializer.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,27 @@ SPDX-License-Identifier: Apache-2.0
Copyright (c) OWASP Foundation. All Rights Reserved.
*/

import { OptPlugError } from '../_optPlug.node/errors'
import type * as OptionalFeature from '../_opt'
import cannotOf from '../_optPlug.node/cannot'
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'


/**
* 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.
*
* `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)

/**
* @throws {@link Serialize.MissingOptionalDependencyError | MissingOptionalDependencyError}
Expand All @@ -38,13 +47,9 @@ 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)
}
}

XmlSerializer satisfies OptionalFeature.Provider
11 changes: 11 additions & 0 deletions src/serialize/xmlSerializer.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ 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'
Expand All @@ -26,6 +27,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: OptionalFeature.Cannot = false

protected _serialize (
normalizedBom: SimpleXml.Element,
{ space }: SerializerOptions = {}
Expand Down Expand Up @@ -91,3 +100,5 @@ export class XmlSerializer extends XmlBaseSerializer {
}
}
}

XmlSerializer satisfies OptionalFeature.Provider
34 changes: 21 additions & 13 deletions src/validation/jsonValidator.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,25 @@ SPDX-License-Identifier: Apache-2.0
Copyright (c) OWASP Foundation. All Rights Reserved.
*/

import { OptPlugError } from '../_optPlug.node/errors'
import type * as OptionalFeature 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'
import { MissingOptionalDependencyError, NotImplementedError } from './errors'
import type { ValidationError } from './types'


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: OptionalFeature.Cannot = cannotOf(makeValidator)

/** @internal */
protected abstract _getSchemaFile (): string | undefined

Expand All @@ -40,18 +51,12 @@ abstract class BaseJsonValidator extends BaseValidator {

async #getValidator (): Promise<Validator> {
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
}
Expand All @@ -71,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 {
Expand Down
25 changes: 16 additions & 9 deletions src/validation/xmlValidator.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,25 @@ SPDX-License-Identifier: Apache-2.0
Copyright (c) OWASP Foundation. All Rights Reserved.
*/

import { OptPlugError } from '../_optPlug.node/errors'
import type * as OptionalFeature 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'
import { MissingOptionalDependencyError, NotImplementedError } from './errors'
import type { ValidationError } from './types'


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: OptionalFeature.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 */
Expand All @@ -38,14 +49,8 @@ export class XmlValidator extends BaseValidator {

async #getValidator (): Promise<Validator> {
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
}
Expand All @@ -65,3 +70,5 @@ export class XmlValidator extends BaseValidator {
return (await this.#getValidator())(data)
}
}

XmlValidator satisfies OptionalFeature.Provider
3 changes: 1 addition & 2 deletions tests/functional/Validation.JsonValidator.node.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
})
Expand Down
3 changes: 1 addition & 2 deletions tests/functional/Validation.XmlValidator.node.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
})
Expand Down
Loading
Loading