Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
9794b31
chore(builder) refacor subschemas to yield also the key
jkowalleck Oct 15, 2026
73217b8
chore(builder): tree shake
jkowalleck Oct 15, 2026
fc25b89
chore(builder): tree shake
jkowalleck Oct 15, 2026
9b4918e
docs
jkowalleck Oct 15, 2026
65c5b30
wip
jkowalleck Oct 15, 2026
282e80f
wip
jkowalleck Oct 15, 2026
e0f4a79
wip
jkowalleck Oct 15, 2026
1c7e670
wip
jkowalleck Oct 15, 2026
c663900
wip
jkowalleck Oct 19, 2026
813601b
wip
jkowalleck Oct 19, 2026
8a4b27a
Merge remote-tracking branch 'origin/2.0-dev' into chore/bundler-rewr…
jkowalleck Oct 19, 2026
f18e8ee
wip
jkowalleck Oct 19, 2026
2045af8
wip
jkowalleck Oct 20, 2026
ac17d29
wip
jkowalleck Oct 20, 2026
95ecf7e
wip
jkowalleck Oct 20, 2026
5ce1691
wip
jkowalleck Oct 20, 2026
c52a3cf
docs
jkowalleck Oct 20, 2026
504cb42
docs
jkowalleck Oct 20, 2026
eb29b49
docs
jkowalleck Oct 20, 2026
c6a0f04
docs
jkowalleck Oct 20, 2026
fb2dc02
fix
jkowalleck Oct 20, 2026
1f72695
tests
jkowalleck Oct 20, 2026
f6f97b9
Merge remote-tracking branch 'origin/2.0-dev' into chore/bundler-rewr…
jkowalleck Oct 20, 2026
3924e37
wip
jkowalleck Oct 20, 2026
de61424
wip
jkowalleck Oct 20, 2026
482e0cc
wip
jkowalleck Oct 20, 2026
3079062
wip
jkowalleck Oct 20, 2026
6494ece
wip
jkowalleck Oct 20, 2026
6e2d9bb
wip
jkowalleck Oct 20, 2026
b35bd3d
wip
jkowalleck Oct 20, 2026
6406ac3
wip
jkowalleck Oct 20, 2026
0c640ff
wip
jkowalleck Oct 20, 2026
aa4d8ed
wip
jkowalleck Oct 20, 2026
044773e
wip
jkowalleck Oct 20, 2026
503f735
wip
jkowalleck Oct 20, 2026
3a3c670
wip
jkowalleck Oct 21, 2026
d48bb4a
wip
jkowalleck Oct 21, 2026
35c8431
wip
jkowalleck Oct 21, 2026
6499eb0
wip
jkowalleck Oct 21, 2026
c5051c3
wip
jkowalleck Oct 21, 2026
f7d7710
wip
jkowalleck Oct 21, 2026
4df9c0f
wip
jkowalleck Oct 21, 2026
c77a3dd
tests
jkowalleck Oct 21, 2026
d944905
wip
jkowalleck Oct 21, 2026
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
2 changes: 1 addition & 1 deletion schema/2.0/cyclonedx-2.0-bundled.min.schema.json

Large diffs are not rendered by default.

169 changes: 102 additions & 67 deletions schema/2.0/cyclonedx-2.0-bundled.schema.json

Large diffs are not rendered by default.

23 changes: 20 additions & 3 deletions tools/src/main/js/bundler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,36 @@ The bundler is split into stages (one module each under `src/`):
* refs to a non-bundled file → path made relative to the output file, kept external
* absolute URIs → left untouched, reported as external
* The root `$id` is rewritten to point at the bundled file name.
2. **Tree-shake** (`src/cleanup.js`) — remove definitions that are not reachable from the root.
*Not implemented yet; currently a no-op.*
2. **Tree-shake** (`src/cleanup.js`) — a mark-and-sweep pass over the bundled schema.
* *Mark*: starting at the root schema, walk all subschemas (`properties`, `items`, `allOf`, …)
and follow every local `$ref`/`$dynamicRef`/`$recursiveRef`. `$defs` are *not* descended into —
definitions are only reached via refs. Refs may point at any JSON Pointer, not just `$defs`
entries, e.g. `#/$defs/<module>/properties/address/properties/street`.
* *Sweep*: everything that was not marked is removed:
* unreached `$defs` entries — also in modules that are referenced as a whole;
* unreached parts (`properties`, `type`, …) of schemas that are not reached themselves
but contain reached parts.
* Schemas that are not reached but contain reached parts are *hollowed*:
only `title`, `description`, `$comment` and the keys leading to the reached parts are kept,
and `"not": {"$comment": …}` is added so the hollow schema never validates anything.
Typical example: an embedded module of which only some `$defs` are referenced.
* Schemas carrying `$anchor`/`$dynamicAnchor`/`$recursiveAnchor` are kept untouched.
3. **Sanity check** (`src/sanity.js`) — verifies every local JSON-Pointer ref resolves in the bundle.
Unresolvable refs are errors; external refs and anchor-based refs are reported as warnings only.
4. **Write** (`cli.js`) — emits the pretty bundle, then strips `$comment` from all subschemas
(property *names* called `$comment` are preserved) and emits the minified bundle.

A report of what was embedded, rewired and treated as external is printed to the console.
A report of what was embedded, rewired and treated as external, and of what the tree-shaker
hollowed and removed, is printed to the console.

## Limitations

* Refs via `$anchor` / `$dynamicAnchor` / `$recursiveAnchor` into bundled files are **not** supported
and cause an error; only JSON-Pointer fragments are rewired.
* Tree-shaking follows JSON-Pointer refs only. Schemas that declare an anchor are kept as they are,
since the tree-shaker cannot tell whether they are referenced.
* Tree-shaking cannot hollow a schema that kept a `not` keyword;
this aborts with a `Collision` error.
* The bundler is not aware of `$id`-based resolution; refs are resolved purely by file path.
* Module names are derived from file basenames (`<name>.schema.json` → `<name>`).
If two schema files — including the entry schema — share a basename, the bundler
Expand Down
5 changes: 3 additions & 2 deletions tools/src/main/js/bundler/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ const main = async (entryFile, includeDir) => {
await writeFile(bundledFile, JSON.stringify(schema, null, 2));

// 4. minify schema
subschemas(schema).forEach(dropComments);
subschemas(schema).forEach(([s]) => dropComments(s));

await writeFile(minifiedFile, JSON.stringify({
...schema,
Expand Down Expand Up @@ -153,8 +153,9 @@ if (import.meta.main) {
console.info(`bundler: embedded (${bundled.embedded.length}):`, bundled.embedded);
console.info(`bundler: external (${bundled.external.length}):`, bundled.external);

console.info(`tree-shake: removed (${shaken.removed.length}):`, shaken.removed);
console.info(`tree-shake: hollowed (${shaken.hollowed.length}):`, shaken.hollowed);
console.info(`tree-shake: voided (${shaken.voided.length}):`, shaken.voided);
console.info(`tree-shake: removed (${shaken.removed.length}):`, shaken.removed);

if (validationWarnings.length) {
console.warn(`validation warnings (${validationWarnings.length}):`, validationWarnings);
Expand Down
30 changes: 20 additions & 10 deletions tools/src/main/js/bundler/src/bundle.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import {basename, dirname, relative, resolve} from 'node:path'
import {basename, dirname, relative, resolve} from 'node:path';
import {ok} from 'node:assert/strict';

import {JSON_SCHEMA_RE, getJsonfile, posixPath, findDuplicateValues} from './helpers/common.js';
import {escapeJsonPointer, PREFIX as JP_PREFIX} from './helpers/json-pointer-spec.js';
import {
escapeJsonPointer, isJsonPointer,
PREFIX as JP_PREFIX, ROOT_POINTER
} from './helpers/json-pointer-spec.js';
import {
ID_KEYWORD, NOT_KEYWORD, SCHEMA_KEYWORD, COMMENT_KEYWORD, DEFS_KEYWORD, REF_KEYWORDS,
subschemas,
refIsAbsolute, refSplit, refJoin, REF_FRAG_SEP,
isSchemaObject, subschemas,
refIsAbsolute, refSplit, refJoin, REF_FRAG_SEP
} from './helpers/json-schema-spec.js';
import {PATH_SEP as URI_PATH_SEP} from './helpers/uri.js';

Expand Down Expand Up @@ -46,9 +50,11 @@ class ModuleNameCollisionError extends Error {
}
}

/** @typedef {import('./helpers/json-schema-spec.js').Schema} Schema */

/**
* @typedef {object} BundleResult
* @property {*} schema
* @property {Schema} schema
* @property {string[]} embedded embedded schema files
* @property {Array<[string, string]>} external external refs
* @property {Array<[string, string]>} rewired rewired refs
Expand Down Expand Up @@ -111,7 +117,7 @@ export const bundle = async (entryFile, includeFiles, targetFile) => {
*
* No URI Decoding is done on refs, as out own cases just don't need it.
*
* @param {*} schema
* @param {Schema} schema
* @param {string} sourceFile
* @returns {void}
*/
Expand All @@ -132,14 +138,14 @@ export const bundle = async (entryFile, includeFiles, targetFile) => {
: sourceFile;
const moduleName = fileModuleNames.get(refFile);
if (moduleName) {
if (frag && !frag.startsWith(JP_PREFIX)) {
if (frag !== null && !isJsonPointer(frag)) {
// currently dont support anchors - only JSON pointer.
throw new RangeError(`Unsupported ref fragment: ${v}`);
}
rewiredRS = {
path: null,
frag: (refFile === entryFile && !frag?.startsWith(FRAG_DEFS_PREFIX)
? ''
? ROOT_POINTER
: `${FRAG_DEFS_PREFIX}${escapeJsonPointer(moduleName)}`
) + (frag ?? '')
};
Expand All @@ -156,10 +162,11 @@ export const bundle = async (entryFile, includeFiles, targetFile) => {
externals.add(rewired);
}
}
subschemas(schema).forEach(s => rewireRefs(s, sourceFile));
subschemas(schema).forEach(([s]) => rewireRefs(s, sourceFile));
}

const schema = await getJsonfile(entryFile);
ok(isSchemaObject(schema), `Not schema: ${entryFile}`);
if (URL.canParse(schema[ID_KEYWORD])) {
schema[ID_KEYWORD] = new URL(
posixPath(relative(entryDir, targetFile)),
Expand All @@ -178,11 +185,14 @@ export const bundle = async (entryFile, includeFiles, targetFile) => {
[NOT_KEYWORD]: {[COMMENT_KEYWORD]: HOLLOW_COMMENT}
};
embeddedMap.set(`${entryFile}${REF_FRAG_SEP}${JP_PREFIX}${DEFS_KEYWORD}`, `${REF_FRAG_SEP}${FRAG_DEFS_PREFIX}${escapeJsonPointer(entryModuleName)}`);
} else {
delete schema[DEFS_KEYWORD][entryModuleName];
}

for (const [includeFile, moduleName] of fileModuleNames) {
if (includeFile === entryFile) continue;
const includeSchema = await getJsonfile(includeFile);
ok(isSchemaObject(includeSchema), `Not schema: ${includeFile}`);
delete includeSchema[SCHEMA_KEYWORD];
delete includeSchema[ID_KEYWORD];
delete includeSchema[COMMENT_KEYWORD];
Expand All @@ -204,6 +214,6 @@ export const bundle = async (entryFile, includeFiles, targetFile) => {
const {path, frag} = refSplit(f);
return [refJoin({path: posixPath(relative(entryDir, path)), frag}), t];
}
),
)
};
};
Loading
Loading