From 704c9e5b442219665d0743fe23f153814b2b5483 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 26 Oct 2026 11:35:18 +0100 Subject: [PATCH 1/6] chore(bundler): metaSchemaValidation and QA Signed-off-by: Jan Kowalleck --- tools/src/main/js/bundler/package-lock.json | 56 +++++ tools/src/main/js/bundler/package.json | 1 + tools/src/main/js/bundler/src/sanity.js | 28 ++- .../js/bundler/test/fixtures/main.schema.json | 15 +- .../modules/used-array-items.schema.json | 21 +- .../snapshot/main-bundled.min.schema.json | 2 +- .../test/snapshot/main-bundled.schema.json | 220 ++++-------------- .../main/js/bundler/test/snapshot/report.json | 132 +++++++---- 8 files changed, 243 insertions(+), 232 deletions(-) diff --git a/tools/src/main/js/bundler/package-lock.json b/tools/src/main/js/bundler/package-lock.json index 864344cc7..ac662c074 100644 --- a/tools/src/main/js/bundler/package-lock.json +++ b/tools/src/main/js/bundler/package-lock.json @@ -8,12 +8,68 @@ "name": "@cyclonedx/schema-bundler", "version": "2.0.0", "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.20" + }, "bin": { "bundle-schemas": "cli.js" }, "engines": { "node": ">=24.2" } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } } } } diff --git a/tools/src/main/js/bundler/package.json b/tools/src/main/js/bundler/package.json index c70735e4b..c4afc6ed6 100644 --- a/tools/src/main/js/bundler/package.json +++ b/tools/src/main/js/bundler/package.json @@ -10,6 +10,7 @@ "bundle-schemas": "./cli.js" }, "dependencies": { + "ajv": "^8.20" }, "scripts": { "cli": "node cli.js", diff --git a/tools/src/main/js/bundler/src/sanity.js b/tools/src/main/js/bundler/src/sanity.js index 127542e82..87c213e31 100644 --- a/tools/src/main/js/bundler/src/sanity.js +++ b/tools/src/main/js/bundler/src/sanity.js @@ -1,4 +1,6 @@ -import {objectGetOwnPath, PATH_NOT_FOUND} from "./helpers/common.js"; +import Ajv2020 from 'ajv/dist/2020.js'; + +import {objectGetOwnPath, PATH_NOT_FOUND} from './helpers/common.js'; import { isSchemaObject, makeLocalRef, @@ -86,18 +88,40 @@ const validateRefs = (schema) => { }; }; +/** + * @param {Readonly} schema + * @returns {ValidationResult} + */ +const validateAgainstMetaSchema = (schema) => { + const warnings = new Set(); + const errors = new Set(); + const ajv = new Ajv2020({allErrors: true}); + const valid = ajv.validateSchema(schema); + if (!valid) { + ajv.errorsText(ajv.errors, {separator: '\n'}) + .split('\n').forEach(e => errors.add(e)); + } + return { + warnings: Array.from(warnings), + errors: Array.from(errors), + }; +}; + /** * @param {Readonly} schema * @returns {ValidationResult} */ export const validateSchema = (schema) => { - const refs = validateRefs(schema) + const refs = validateRefs(schema); + const meta = validateAgainstMetaSchema(schema); return { warnings: [ ...refs.warnings, + ...meta.warnings, ], errors: [ ...refs.errors, + ...meta.errors, ], } }; diff --git a/tools/src/main/js/bundler/test/fixtures/main.schema.json b/tools/src/main/js/bundler/test/fixtures/main.schema.json index b668fef21..3e10d406b 100644 --- a/tools/src/main/js/bundler/test/fixtures/main.schema.json +++ b/tools/src/main/js/bundler/test/fixtures/main.schema.json @@ -9,7 +9,9 @@ "whole" ], "examples": [ - {"whole": {}} + { + "whole": {} + } ], "$comment": "FIXTURE: root schema; entry point of the bundle. This $comment is the only one that survives minification. The root title/description must survive in both outputs. The root `required`/`examples` are data arrays inside the (reached) root schema and must survive the tree-shaker untouched.", "properties": { @@ -83,10 +85,17 @@ } }, "middle": { - "$comment": "Ref INTO a schema array (allOf/1) of a module -> siblings allOf/0 and allOf/2 must become void placeholders, not null; the index must stay 1.", + "$comment": "Ref INTO a schema array (allOf/1 and allOf/3) of a module -> siblings allOf/0 and allOf/2 must become void placeholders or stay boolean, not null; the indexes must stay; the rest can be safely removed.", "title": "Middle", "description": "The middle part of the module's combined schema.", - "$ref": "modules/used-array-items.schema.json#/properties/combined/allOf/1" + "anyOf": [ + { + "$ref": "modules/used-array-items.schema.json#/properties/combined/allOf/1" + }, + { + "$ref": "modules/used-array-items.schema.json#/properties/combined/allOf/3" + } + ] }, "lastChoice": { "$comment": "Ref INTO the last item of a oneOf -> the preceding item must become a void placeholder.", diff --git a/tools/src/main/js/bundler/test/fixtures/modules/used-array-items.schema.json b/tools/src/main/js/bundler/test/fixtures/modules/used-array-items.schema.json index a07590bed..b984b25fb 100644 --- a/tools/src/main/js/bundler/test/fixtures/modules/used-array-items.schema.json +++ b/tools/src/main/js/bundler/test/fixtures/modules/used-array-items.schema.json @@ -7,7 +7,7 @@ "type": "object", "properties": { "combined": { - "$comment": "Only allOf/1 is referenced from main. allOf/0 and allOf/2 are not reached -> they must become void placeholders, never JSON null, so that the index of allOf/1 stays stable. 'combined' itself is not reached -> hollowed.", + "$comment": "Only allOf/1 and allOf/3 are referenced from main. allOf/0 and allOf/2 are not reached -> they must become void placeholders or stay boolean, never JSON null, so that the index of allOf/3 stays stable; the rest can be truncated. 'combined' itself is not reached -> hollowed.", "title": "Combined", "description": "A composition of which only the middle part is referenced.", "allOf": [ @@ -26,7 +26,7 @@ }, { "$comment": "allOf/1: referenced from main via #/properties/combined/allOf/1 -> kept as a whole, including its internal ref to $defs/keptViaArray.", - "title": "Combined part 1", + "title": "Combined part 2", "description": "The middle part.", "type": "object", "properties": { @@ -37,11 +37,20 @@ } } }, + false, + true, { - "$comment": "allOf/2: NOT referenced -> void placeholder. Its ref must NOT make unused.schema.json reachable.", - "title": "Combined part 2", - "description": "The last part.", - "$ref": "unused.schema.json" + "$comment": "allOf/4: NOT referenced ano nothing used after -> removed.", + "title": "Combined part 6", + "description": "The end part.", + "type": "object", + "properties": { + "a": { + "title": "A", + "description": "An unreferenced value.", + "type": "string" + } + } } ] }, diff --git a/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json b/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json index afb8f585d..1961da8cf 100644 --- a/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json +++ b/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json @@ -1 +1 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"http://example.com/test/main-bundled.min.schema.json","$comment":"FIXTURE: root schema; entry point of the bundle. This $comment is the only one that survives minification. The root title/description must survive in both outputs. The root `required`/`examples` are data arrays inside the (reached) root schema and must survive the tree-shaker untouched.","title":"Main","description":"Root document of the bundler test fixtures.","type":"object","required":["whole"],"examples":[{"whole":{}}],"properties":{"$comment":{"type":"string","title":"Comment"},"whole":{"title":"Whole","description":"A module referenced in its entirety.","$ref":"#/$defs/used-whole"},"wholeData":{"title":"Whole data","description":"A data-heavy module referenced in its entirety.","$ref":"#/$defs/used-whole-data"},"sub":{"title":"Sub","description":"A single definition of another module.","$ref":"#/$defs/used-subdefs-only/$defs/kept"},"local":{"title":"Local","description":"A definition of this very schema.","$ref":"#/$defs/main/$defs/mainLocal"},"composed":{"title":"Composed","description":"An object assembled from parts of another module.","type":"object","$defs":{"rootNestedDropped":{"title":"Root nested dropped","description":"An unreferenced nested definition of the root.","type":"null"}},"properties":{"street":{"title":"Street","description":"The street, taken from the module's address.","$ref":"#/$defs/used-properties-only/properties/address/properties/street"},"latitude":{"title":"Latitude","description":"The latitude, taken from the module's address.geo.","$ref":"#/$defs/used-properties-only/properties/address/properties/geo/properties/lat"},"contact":{"title":"Contact","description":"The contact block of the module, as is.","$ref":"#/$defs/used-properties-only/properties/contact"},"nested":{"title":"Nested","description":"A value defined in a property-level $defs.","$ref":"#/$defs/used-whole/properties/withDefs/$defs/nestedKept"}}},"middle":{"title":"Middle","description":"The middle part of the module's combined schema.","$ref":"#/$defs/used-array-items/properties/combined/allOf/1"},"lastChoice":{"title":"Last choice","description":"The last alternative of the module's choice.","$ref":"#/$defs/used-array-items/properties/choice/oneOf/1"},"anyValue":{"title":"Any value","description":"Accepts anything.","$ref":"#/$defs/used-subdefs-only/$defs/keptEmpty"},"todoValue":{"title":"Todo value","description":"A placeholder.","$ref":"#/$defs/used-subdefs-only/$defs/keptCommentOnly"},"annotatedValue":{"title":"Annotated value","description":"An annotation-only schema.","$ref":"#/$defs/used-subdefs-only/$defs/keptAnnotationsOnly"}},"$defs":{"dynamic-anchor":{"$dynamicAnchor":"node","title":"Dynamic anchor","description":"A recursive tree node, addressable via a dynamic anchor.","type":"object","properties":{"children":{"title":"Children","description":"The child nodes of this node.","type":"array","items":{"title":"Child","description":"A nested node.","$ref":"#/$defs/dynamic-anchor"}}}},"main":{"$defs":{"mainLocal":{"title":"Main local","description":"A local string definition of the root schema.","type":"string"},"mainUnused":{"title":"Main unused","description":"A definition nothing refers to.","type":"null"}},"not":{}},"unused":{"title":"Unused","description":"A module that nothing reachable refers to.","type":"object","properties":{"pullsNothingIn":{"title":"Pulls nothing in","description":"A flag defined by another module.","$ref":"#/$defs/used-whole/$defs/unused"}}},"used-array-items":{"title":"Used array items","description":"A module of which only individual array items and a non-schema value are referenced.","type":"object","properties":{"combined":{"title":"Combined","description":"A composition of which only the middle part is referenced.","allOf":[{"title":"Combined part 0","description":"The first part.","type":"object","properties":{"a":{"title":"A","description":"An unreferenced value.","type":"string"}}},{"title":"Combined part 1","description":"The middle part.","type":"object","properties":{"b":{"title":"B","description":"A value defined in this module.","$ref":"#/$defs/used-array-items/$defs/keptViaArray"}}},{"title":"Combined part 2","description":"The last part.","$ref":"#/$defs/unused"}]},"choice":{"title":"Choice","description":"A choice of which only the last alternative is referenced.","oneOf":[{"title":"Choice 0","description":"The first alternative.","type":"null","$defs":{"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}},{"title":"Choice 1","description":"The second alternative.","type":"integer","$defs":{"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}}],"examples":[null,1,2,3]}},"$defs":{"keptViaArray":{"title":"Kept via array","description":"A boolean value.","type":"boolean"},"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}},"used-properties-only":{"title":"Used properties only","description":"A module of which only nested properties are referenced.","type":"object","properties":{"address":{"title":"Address","description":"A postal address.","type":"object","properties":{"street":{"title":"Street","description":"Street name and number.","type":"string"},"city":{"title":"City","description":"Name of the city.","type":"string"},"geo":{"title":"Geo","description":"Geographic coordinates.","type":"object","properties":{"lat":{"title":"Latitude","description":"Latitude in degrees.","$ref":"#/$defs/used-properties-only/$defs/degrees"},"lon":{"title":"Longitude","description":"Longitude in degrees.","$ref":"#/$defs/used-properties-only/$defs/degrees"}}}}},"contact":{"title":"Contact","description":"Contact details.","type":"object","properties":{"email":{"title":"E-mail","description":"An e-mail address.","type":"string","format":"email"},"phone":{"title":"Phone","description":"A phone number.","type":"string"}}},"unusedTopLevel":{"title":"Unused top level","description":"A property nothing refers to.","$ref":"#/$defs/unused"}},"$defs":{"degrees":{"title":"Degrees","description":"An angle in degrees.","type":"number","minimum":-180,"maximum":180},"unusedDef":{"title":"Unused def","description":"A definition nothing reachable refers to.","type":"null"}}},"used-subdefs-only":{"title":"Used sub-defs only","description":"A module of which only individual definitions are referenced.","type":"object","properties":{"body":{"title":"Body","description":"The body of this module.","$ref":"#/$defs/unused"}},"$defs":{"kept":{"title":"Kept","description":"A definition that is reached directly from the root schema.","type":"object","properties":{"shared":{"title":"Shared","description":"A value defined by another module.","$ref":"#/$defs/used-whole/$defs/sharedByOther"},"fromMain":{"title":"From main","description":"A value defined by the root schema.","$ref":"#/$defs/main/$defs/mainLocal"},"chain":{"title":"Chain","description":"A value defined further down in this module.","$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"}}},"keptEmpty":{},"keptCommentOnly":{},"keptAnnotationsOnly":{"title":"Kept annotations only","description":"A schema that is just an annotation."},"keptTransitively":{"title":"Kept transitively","description":"A string value.","type":"string"},"dropped":{"title":"Dropped","description":"A definition nothing reachable refers to.","type":"null"}}},"used-whole":{"title":"Used whole","description":"A module that is referenced in its entirety.","type":"object","properties":{"name":{"title":"Name","description":"A display name.","type":"string","$defs":{"nestedDropped":{"title":"Nested dropped","description":"An unreferenced nested definition.","type":"null"}}},"withDefs":{"title":"With defs","description":"A property that hosts its own definitions.","anyOf":[{"title":"Is string","type":"string"},{"title":"Non-zero number","type":"number","if":{"const":0,"$defs":{"unused":{"title":"Unused","type":"null"}}},"then":false,"else":true,"$defs":{"unusedPositive":{"title":"Number > 0","type":"number","exclusiveMinimum":0},"unusedNegative":{"title":"Number < 0","type":"number","exclusiveMaximum":0}}}],"$defs":{"nestedKept":{"title":"Nested kept","description":"A referenced nested definition.","type":"integer"},"nestedDropped":{"title":"Nested dropped","description":"An unreferenced nested definition.","type":"null"}}},"inner":{"title":"Inner","description":"Sibling annotations next to a $ref: title/description are kept, while the sibling $comment is removed in the minified output.","$ref":"#/$defs/used-whole/$defs/inner"},"license":{"title":"License","description":"The license of this item.","$ref":"externals/not-bundled.schema.json#/$defs/licenseId"},"remote":{"title":"Remote","description":"A thing defined by a remote schema.","$ref":"https://example.com/remote.schema.json#/$defs/thing"},"inArray":{"title":"In array","description":"A composition of several referenced schemas.","allOf":[{"$ref":"#/$defs/used-whole/$defs/inner"},{"$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"},{"$ref":"externals/not-bundled.schema.json#/$defs/licenseId"}]}},"$defs":{"inner":{"title":"Inner","description":"An integer value.","type":"integer"},"unused":{"title":"Unused","description":"A flag.","type":"boolean"},"sharedByOther":{"title":"Shared by other","description":"A numeric value shared across modules.","type":"number"}}},"used-whole-data":{"title":"Used whole data","description":"A module referenced in its entirety that carries data-bearing keywords inside reached schemas.","type":"object","required":["status","level"],"examples":[{"status":"on","level":1},{"status":"off","level":2}],"default":{"status":"on","level":1},"properties":{"status":{"title":"Status","description":"A status value.","type":"string","enum":["on","off"],"meta:enum":{"on":"It is on.","off":"It is off."}},"level":{"title":"Level","description":"A fixed level.","type":"integer","const":1,"examples":[1],"default":1},"flags":{"title":"Flags","description":"An object with boolean-schema properties.","type":"object","required":["anything"],"properties":{"anything":true,"nothing":false},"additionalProperties":false},"tags":{"title":"Tags","description":"A list of tags.","type":"array","uniqueItems":true,"minItems":0,"items":{"title":"Tag","description":"A single tag.","type":"string"}}},"$defs":{"unusedDef":{"title":"Unused def","description":"A definition nothing reachable refers to.","type":"null"}}}}} \ No newline at end of file +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"http://example.com/test/main-bundled.min.schema.json","$comment":"FIXTURE: root schema; entry point of the bundle. This $comment is the only one that survives minification. The root title/description must survive in both outputs. The root `required`/`examples` are data arrays inside the (reached) root schema and must survive the tree-shaker untouched.","title":"Main","description":"Root document of the bundler test fixtures.","type":"object","required":["whole"],"examples":[{"whole":{}}],"properties":{"$comment":{"type":"string","title":"Comment"},"whole":{"title":"Whole","description":"A module referenced in its entirety.","$ref":"#/$defs/used-whole"},"wholeData":{"title":"Whole data","description":"A data-heavy module referenced in its entirety.","$ref":"#/$defs/used-whole-data"},"sub":{"title":"Sub","description":"A single definition of another module.","$ref":"#/$defs/used-subdefs-only/$defs/kept"},"local":{"title":"Local","description":"A definition of this very schema.","$ref":"#/$defs/main/$defs/mainLocal"},"composed":{"title":"Composed","description":"An object assembled from parts of another module.","type":"object","properties":{"street":{"title":"Street","description":"The street, taken from the module's address.","$ref":"#/$defs/used-properties-only/properties/address/properties/street"},"latitude":{"title":"Latitude","description":"The latitude, taken from the module's address.geo.","$ref":"#/$defs/used-properties-only/properties/address/properties/geo/properties/lat"},"contact":{"title":"Contact","description":"The contact block of the module, as is.","$ref":"#/$defs/used-properties-only/properties/contact"},"nested":{"title":"Nested","description":"A value defined in a property-level $defs.","$ref":"#/$defs/used-whole/properties/withDefs/$defs/nestedKept"}}},"middle":{"title":"Middle","description":"The middle part of the module's combined schema.","anyOf":[{"$ref":"#/$defs/used-array-items/properties/combined/allOf/1"},{"$ref":"#/$defs/used-array-items/properties/combined/allOf/3"}]},"lastChoice":{"title":"Last choice","description":"The last alternative of the module's choice.","$ref":"#/$defs/used-array-items/properties/choice/oneOf/1"},"anyValue":{"title":"Any value","description":"Accepts anything.","$ref":"#/$defs/used-subdefs-only/$defs/keptEmpty"},"todoValue":{"title":"Todo value","description":"A placeholder.","$ref":"#/$defs/used-subdefs-only/$defs/keptCommentOnly"},"annotatedValue":{"title":"Annotated value","description":"An annotation-only schema.","$ref":"#/$defs/used-subdefs-only/$defs/keptAnnotationsOnly"}},"$defs":{"dynamic-anchor":{"$dynamicAnchor":"node","title":"Dynamic anchor","description":"A recursive tree node, addressable via a dynamic anchor.","type":"object","properties":{"children":{"title":"Children","description":"The child nodes of this node.","type":"array","items":{"title":"Child","description":"A nested node.","$ref":"#/$defs/dynamic-anchor"}}}},"main":{"$defs":{"mainLocal":{"title":"Main local","description":"A local string definition of the root schema.","type":"string"}},"not":{}},"used-array-items":{"title":"Used array items","description":"A module of which only individual array items and a non-schema value are referenced.","properties":{"combined":{"title":"Combined","description":"A composition of which only the middle part is referenced.","allOf":[{"not":{}},{"title":"Combined part 2","description":"The middle part.","type":"object","properties":{"b":{"title":"B","description":"A value defined in this module.","$ref":"#/$defs/used-array-items/$defs/keptViaArray"}}},false,true],"not":{}},"choice":{"title":"Choice","description":"A choice of which only the last alternative is referenced.","oneOf":[{"not":{}},{"title":"Choice 1","description":"The second alternative.","type":"integer"}],"not":{}}},"$defs":{"keptViaArray":{"title":"Kept via array","description":"A boolean value.","type":"boolean"}},"not":{}},"used-properties-only":{"title":"Used properties only","description":"A module of which only nested properties are referenced.","properties":{"address":{"title":"Address","description":"A postal address.","properties":{"street":{"title":"Street","description":"Street name and number.","type":"string"},"geo":{"title":"Geo","description":"Geographic coordinates.","properties":{"lat":{"title":"Latitude","description":"Latitude in degrees.","$ref":"#/$defs/used-properties-only/$defs/degrees"}},"not":{}}},"not":{}},"contact":{"title":"Contact","description":"Contact details.","type":"object","properties":{"email":{"title":"E-mail","description":"An e-mail address.","type":"string","format":"email"},"phone":{"title":"Phone","description":"A phone number.","type":"string"}}}},"$defs":{"degrees":{"title":"Degrees","description":"An angle in degrees.","type":"number","minimum":-180,"maximum":180}},"not":{}},"used-subdefs-only":{"title":"Used sub-defs only","description":"A module of which only individual definitions are referenced.","$defs":{"kept":{"title":"Kept","description":"A definition that is reached directly from the root schema.","type":"object","properties":{"shared":{"title":"Shared","description":"A value defined by another module.","$ref":"#/$defs/used-whole/$defs/sharedByOther"},"fromMain":{"title":"From main","description":"A value defined by the root schema.","$ref":"#/$defs/main/$defs/mainLocal"},"chain":{"title":"Chain","description":"A value defined further down in this module.","$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"}}},"keptEmpty":{},"keptCommentOnly":{},"keptAnnotationsOnly":{"title":"Kept annotations only","description":"A schema that is just an annotation."},"keptTransitively":{"title":"Kept transitively","description":"A string value.","type":"string"}},"not":{}},"used-whole":{"title":"Used whole","description":"A module that is referenced in its entirety.","type":"object","properties":{"name":{"title":"Name","description":"A display name.","type":"string"},"withDefs":{"title":"With defs","description":"A property that hosts its own definitions.","anyOf":[{"title":"Is string","type":"string"},{"title":"Non-zero number","type":"number","if":{"const":0},"then":false,"else":true}],"$defs":{"nestedKept":{"title":"Nested kept","description":"A referenced nested definition.","type":"integer"}}},"inner":{"title":"Inner","description":"Sibling annotations next to a $ref: title/description are kept, while the sibling $comment is removed in the minified output.","$ref":"#/$defs/used-whole/$defs/inner"},"license":{"title":"License","description":"The license of this item.","$ref":"externals/not-bundled.schema.json#/$defs/licenseId"},"remote":{"title":"Remote","description":"A thing defined by a remote schema.","$ref":"https://example.com/remote.schema.json#/$defs/thing"},"inArray":{"title":"In array","description":"A composition of several referenced schemas.","allOf":[{"$ref":"#/$defs/used-whole/$defs/inner"},{"$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"},{"$ref":"externals/not-bundled.schema.json#/$defs/licenseId"}]}},"$defs":{"inner":{"title":"Inner","description":"An integer value.","type":"integer"},"sharedByOther":{"title":"Shared by other","description":"A numeric value shared across modules.","type":"number"}}},"used-whole-data":{"title":"Used whole data","description":"A module referenced in its entirety that carries data-bearing keywords inside reached schemas.","type":"object","required":["status","level"],"examples":[{"status":"on","level":1},{"status":"off","level":2}],"default":{"status":"on","level":1},"properties":{"status":{"title":"Status","description":"A status value.","type":"string","enum":["on","off"],"meta:enum":{"on":"It is on.","off":"It is off."}},"level":{"title":"Level","description":"A fixed level.","type":"integer","const":1,"examples":[1],"default":1},"flags":{"title":"Flags","description":"An object with boolean-schema properties.","type":"object","required":["anything"],"properties":{"anything":true,"nothing":false},"additionalProperties":false},"tags":{"title":"Tags","description":"A list of tags.","type":"array","uniqueItems":true,"minItems":0,"items":{"title":"Tag","description":"A single tag.","type":"string"}}}}}} \ No newline at end of file diff --git a/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json b/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json index c39871a59..e95a7a642 100644 --- a/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json +++ b/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json @@ -48,14 +48,6 @@ "title": "Composed", "description": "An object assembled from parts of another module.", "type": "object", - "$defs": { - "rootNestedDropped": { - "$comment": "Nested $defs inside a REACHED property of the ROOT schema. Unreferenced -> must be removed; the root and its properties are reached, yet their $defs still need sweeping. The then-empty `$defs` object is removed too.", - "title": "Root nested dropped", - "description": "An unreferenced nested definition of the root.", - "type": "null" - } - }, "properties": { "street": { "$comment": "Ref into a nested property of the module -> #/properties/address/properties/street.", @@ -84,10 +76,17 @@ } }, "middle": { - "$comment": "Ref INTO a schema array (allOf/1) of a module -> siblings allOf/0 and allOf/2 must become void placeholders, not null; the index must stay 1.", + "$comment": "Ref INTO a schema array (allOf/1 and allOf/3) of a module -> siblings allOf/0 and allOf/2 must become void placeholders or stay boolean, not null; the indexes must stay; the rest can be safely removed.", "title": "Middle", "description": "The middle part of the module's combined schema.", - "$ref": "#/$defs/used-array-items/properties/combined/allOf/1" + "anyOf": [ + { + "$ref": "#/$defs/used-array-items/properties/combined/allOf/1" + }, + { + "$ref": "#/$defs/used-array-items/properties/combined/allOf/3" + } + ] }, "lastChoice": { "$comment": "Ref INTO the last item of a oneOf -> the preceding item must become a void placeholder.", @@ -141,57 +140,29 @@ "title": "Main local", "description": "A local string definition of the root schema.", "type": "string" - }, - "mainUnused": { - "$comment": "Never referenced -> shaken away from #/$defs/main/$defs.", - "title": "Main unused", - "description": "A definition nothing refers to.", - "type": "null" } }, "not": { - "$comment": "This schema is hollow: it is as a container for definitions. Itself must not be used for validation." - } - }, - "unused": { - "title": "Unused", - "description": "A module that nothing reachable refers to.", - "type": "object", - "properties": { - "pullsNothingIn": { - "$comment": "Even though this refs a module, it is itself unreachable and must not influence shaking.", - "title": "Pulls nothing in", - "description": "A flag defined by another module.", - "$ref": "#/$defs/used-whole/$defs/unused" - } + "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." } }, "used-array-items": { "title": "Used array items", "description": "A module of which only individual array items and a non-schema value are referenced.", - "type": "object", "properties": { "combined": { - "$comment": "Only allOf/1 is referenced from main. allOf/0 and allOf/2 are not reached -> they must become void placeholders, never JSON null, so that the index of allOf/1 stays stable. 'combined' itself is not reached -> hollowed.", + "$comment": "Only allOf/1 and allOf/3 are referenced from main. allOf/0 and allOf/2 are not reached -> they must become void placeholders or stay boolean, never JSON null, so that the index of allOf/3 stays stable; the rest can be truncated. 'combined' itself is not reached -> hollowed.", "title": "Combined", "description": "A composition of which only the middle part is referenced.", "allOf": [ { - "$comment": "allOf/0: NOT referenced -> void placeholder.", - "title": "Combined part 0", - "description": "The first part.", - "type": "object", - "properties": { - "a": { - "title": "A", - "description": "An unreferenced value.", - "type": "string" - } + "not": { + "$comment": "This schema is void: it is not referenced and was removed. It only remains as a placeholder to keep sibling positions stable. Itself must not be used for validation." } }, { "$comment": "allOf/1: referenced from main via #/properties/combined/allOf/1 -> kept as a whole, including its internal ref to $defs/keptViaArray.", - "title": "Combined part 1", + "title": "Combined part 2", "description": "The middle part.", "type": "object", "properties": { @@ -202,13 +173,12 @@ } } }, - { - "$comment": "allOf/2: NOT referenced -> void placeholder. Its ref must NOT make unused.schema.json reachable.", - "title": "Combined part 2", - "description": "The last part.", - "$ref": "#/$defs/unused" - } - ] + false, + true + ], + "not": { + "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." + } }, "choice": { "$comment": "Only the LAST item of oneOf is referenced -> oneOf/0 becomes a void placeholder, oneOf/1 is kept. The examples are not referenced -> remove", @@ -216,40 +186,20 @@ "description": "A choice of which only the last alternative is referenced.", "oneOf": [ { - "$comment": "oneOf/0: NOT referenced -> void placeholder.", - "title": "Choice 0", - "description": "The first alternative.", - "type": "null", - "$defs": { - "droppedDef": { - "$comment": "Not reachable -> shaken away.", - "title": "Dropped def", - "description": "A definition nothing reachable refers to.", - "type": "null" - } + "not": { + "$comment": "This schema is void: it is not referenced and was removed. It only remains as a placeholder to keep sibling positions stable. Itself must not be used for validation." } }, { "$comment": "oneOf/1: referenced from main via #/properties/choice/oneOf/1.", "title": "Choice 1", "description": "The second alternative.", - "type": "integer", - "$defs": { - "droppedDef": { - "$comment": "Not reachable -> shaken away.", - "title": "Dropped def", - "description": "A definition nothing reachable refers to.", - "type": "null" - } - } + "type": "integer" } ], - "examples": [ - null, - 1, - 2, - 3 - ] + "not": { + "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." + } } }, "$defs": { @@ -258,25 +208,20 @@ "title": "Kept via array", "description": "A boolean value.", "type": "boolean" - }, - "droppedDef": { - "$comment": "Not reachable -> shaken away.", - "title": "Dropped def", - "description": "A definition nothing reachable refers to.", - "type": "null" } + }, + "not": { + "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." } }, "used-properties-only": { "title": "Used properties only", "description": "A module of which only nested properties are referenced.", - "type": "object", "properties": { "address": { "$comment": "Reached only through its nested children, never as a whole -> the container stays hollow, only the referenced children survive.", "title": "Address", "description": "A postal address.", - "type": "object", "properties": { "street": { "$comment": "Reached from main via #/properties/address/properties/street.", @@ -284,32 +229,25 @@ "description": "Street name and number.", "type": "string" }, - "city": { - "$comment": "Not referenced -> dropped although its siblings are kept.", - "title": "City", - "description": "Name of the city.", - "type": "string" - }, "geo": { "$comment": "Reached from main via a three-level deep pointer into #/properties/address/properties/geo/properties/lat.", "title": "Geo", "description": "Geographic coordinates.", - "type": "object", "properties": { "lat": { "$comment": "Deeply nested leaf that is referenced directly from main. Has an internal ref -> transitively keeps $defs/degrees.", "title": "Latitude", "description": "Latitude in degrees.", "$ref": "#/$defs/used-properties-only/$defs/degrees" - }, - "lon": { - "$comment": "Deeply nested leaf that is NOT referenced -> dropped.", - "title": "Longitude", - "description": "Longitude in degrees.", - "$ref": "#/$defs/used-properties-only/$defs/degrees" } + }, + "not": { + "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." } } + }, + "not": { + "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." } }, "contact": { @@ -331,12 +269,6 @@ "type": "string" } } - }, - "unusedTopLevel": { - "$comment": "Not referenced -> dropped together with everything it refers to; must NOT make unused.schema.json reachable.", - "title": "Unused top level", - "description": "A property nothing refers to.", - "$ref": "#/$defs/unused" } }, "$defs": { @@ -347,27 +279,15 @@ "type": "number", "minimum": -180, "maximum": 180 - }, - "unusedDef": { - "$comment": "Not reachable -> shaken away.", - "title": "Unused def", - "description": "A definition nothing reachable refers to.", - "type": "null" } + }, + "not": { + "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." } }, "used-subdefs-only": { "title": "Used sub-defs only", "description": "A module of which only individual definitions are referenced.", - "type": "object", - "properties": { - "body": { - "$comment": "Lives in the dropped body, so this ref must NOT make unused.schema.json reachable.", - "title": "Body", - "description": "The body of this module.", - "$ref": "#/$defs/unused" - } - }, "$defs": { "kept": { "$comment": "Reached from main. Exercises three ref flavours.", @@ -409,13 +329,10 @@ "title": "Kept transitively", "description": "A string value.", "type": "string" - }, - "dropped": { - "$comment": "Not reachable -> shaken away.", - "title": "Dropped", - "description": "A definition nothing reachable refers to.", - "type": "null" } + }, + "not": { + "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." } }, "used-whole": { @@ -426,15 +343,7 @@ "name": { "title": "Name", "description": "A display name.", - "type": "string", - "$defs": { - "nestedDropped": { - "$comment": "Lives in $defs of a REACHED property (used-whole is used as a whole). Nothing refs it -> removed; the then-empty `$defs` object is removed too.", - "title": "Nested dropped", - "description": "An unreferenced nested definition.", - "type": "null" - } - } + "type": "string" }, "withDefs": { "$comment": "A REACHED property carrying nested $defs of which exactly one is referenced from main.", @@ -450,31 +359,10 @@ "type": "number", "$comment": "if/then/else are intentionally NOT schema objects.", "if": { - "const": 0, - "$defs": { - "unused": { - "$comment": "unreferenced -> shaken away.", - "title": "Unused", - "type": "null" - } - } + "const": 0 }, "then": false, - "else": true, - "$defs": { - "unusedPositive": { - "$comment": "unreferenced -> shaken away.", - "title": "Number > 0", - "type": "number", - "exclusiveMinimum": 0 - }, - "unusedNegative": { - "$comment": "unreferenced -> shaken away.", - "title": "Number < 0", - "type": "number", - "exclusiveMaximum": 0 - } - } + "else": true } ], "$defs": { @@ -483,12 +371,6 @@ "title": "Nested kept", "description": "A referenced nested definition.", "type": "integer" - }, - "nestedDropped": { - "$comment": "Sibling of a kept nested def, itself unreferenced -> removed.", - "title": "Nested dropped", - "description": "An unreferenced nested definition.", - "type": "null" } } }, @@ -533,12 +415,6 @@ "description": "An integer value.", "type": "integer" }, - "unused": { - "$comment": "Not reachable -> shaken away.", - "title": "Unused", - "description": "A flag.", - "type": "boolean" - }, "sharedByOther": { "$comment": "Cross-referenced from used-subdefs-only#/$defs/kept.", "title": "Shared by other", @@ -622,14 +498,6 @@ "type": "string" } } - }, - "$defs": { - "unusedDef": { - "$comment": "The ONLY thing the tree-shaker is allowed to drop from this module.", - "title": "Unused def", - "description": "A definition nothing reachable refers to.", - "type": "null" - } } } } diff --git a/tools/src/main/js/bundler/test/snapshot/report.json b/tools/src/main/js/bundler/test/snapshot/report.json index 3a5e3dad3..681609628 100644 --- a/tools/src/main/js/bundler/test/snapshot/report.json +++ b/tools/src/main/js/bundler/test/snapshot/report.json @@ -40,98 +40,142 @@ ], "rewired": [ [ - "modules/used-whole.schema.json", - "#/$defs/used-whole" - ], - [ - "modules/used-whole-data.schema.json", - "#/$defs/used-whole-data" - ], - [ - "modules/used-subdefs-only.schema.json#/$defs/kept", - "#/$defs/used-subdefs-only/$defs/kept" + "externals/not-bundled.schema.json#/$defs/licenseId", + "externals/not-bundled.schema.json#/$defs/licenseId" ], [ "main.schema.json#/$defs/mainLocal", "#/$defs/main/$defs/mainLocal" ], [ - "modules/used-properties-only.schema.json#/properties/address/properties/street", - "#/$defs/used-properties-only/properties/address/properties/street" + "modules/dynamic-anchor.schema.json", + "#/$defs/dynamic-anchor" ], [ - "modules/used-properties-only.schema.json#/properties/address/properties/geo/properties/lat", - "#/$defs/used-properties-only/properties/address/properties/geo/properties/lat" + "modules/unused.schema.json", + "#/$defs/unused" ], [ - "modules/used-properties-only.schema.json#/properties/contact", - "#/$defs/used-properties-only/properties/contact" + "modules/used-array-items.schema.json#/$defs/keptViaArray", + "#/$defs/used-array-items/$defs/keptViaArray" ], [ - "modules/used-whole.schema.json#/properties/withDefs/$defs/nestedKept", - "#/$defs/used-whole/properties/withDefs/$defs/nestedKept" + "modules/used-array-items.schema.json#/properties/choice/oneOf/1", + "#/$defs/used-array-items/properties/choice/oneOf/1" ], [ "modules/used-array-items.schema.json#/properties/combined/allOf/1", "#/$defs/used-array-items/properties/combined/allOf/1" ], [ - "modules/used-array-items.schema.json#/properties/choice/oneOf/1", - "#/$defs/used-array-items/properties/choice/oneOf/1" - ], - [ - "modules/used-subdefs-only.schema.json#/$defs/keptEmpty", - "#/$defs/used-subdefs-only/$defs/keptEmpty" + "modules/used-array-items.schema.json#/properties/combined/allOf/3", + "#/$defs/used-array-items/properties/combined/allOf/3" ], [ - "modules/used-subdefs-only.schema.json#/$defs/keptCommentOnly", - "#/$defs/used-subdefs-only/$defs/keptCommentOnly" + "modules/used-properties-only.schema.json#/$defs/degrees", + "#/$defs/used-properties-only/$defs/degrees" ], [ - "modules/used-subdefs-only.schema.json#/$defs/keptAnnotationsOnly", - "#/$defs/used-subdefs-only/$defs/keptAnnotationsOnly" + "modules/used-properties-only.schema.json#/properties/address/properties/geo/properties/lat", + "#/$defs/used-properties-only/properties/address/properties/geo/properties/lat" ], [ - "modules/dynamic-anchor.schema.json", - "#/$defs/dynamic-anchor" + "modules/used-properties-only.schema.json#/properties/address/properties/street", + "#/$defs/used-properties-only/properties/address/properties/street" ], [ - "modules/used-whole.schema.json#/$defs/unused", - "#/$defs/used-whole/$defs/unused" + "modules/used-properties-only.schema.json#/properties/contact", + "#/$defs/used-properties-only/properties/contact" ], [ - "modules/used-array-items.schema.json#/$defs/keptViaArray", - "#/$defs/used-array-items/$defs/keptViaArray" + "modules/used-subdefs-only.schema.json#/$defs/kept", + "#/$defs/used-subdefs-only/$defs/kept" ], [ - "modules/unused.schema.json", - "#/$defs/unused" + "modules/used-subdefs-only.schema.json#/$defs/keptAnnotationsOnly", + "#/$defs/used-subdefs-only/$defs/keptAnnotationsOnly" ], [ - "modules/used-properties-only.schema.json#/$defs/degrees", - "#/$defs/used-properties-only/$defs/degrees" + "modules/used-subdefs-only.schema.json#/$defs/keptCommentOnly", + "#/$defs/used-subdefs-only/$defs/keptCommentOnly" ], [ - "modules/used-whole.schema.json#/$defs/sharedByOther", - "#/$defs/used-whole/$defs/sharedByOther" + "modules/used-subdefs-only.schema.json#/$defs/keptEmpty", + "#/$defs/used-subdefs-only/$defs/keptEmpty" ], [ "modules/used-subdefs-only.schema.json#/$defs/keptTransitively", "#/$defs/used-subdefs-only/$defs/keptTransitively" ], + [ + "modules/used-whole-data.schema.json", + "#/$defs/used-whole-data" + ], + [ + "modules/used-whole.schema.json", + "#/$defs/used-whole" + ], [ "modules/used-whole.schema.json#/$defs/inner", "#/$defs/used-whole/$defs/inner" ], [ - "externals/not-bundled.schema.json#/$defs/licenseId", - "externals/not-bundled.schema.json#/$defs/licenseId" + "modules/used-whole.schema.json#/$defs/sharedByOther", + "#/$defs/used-whole/$defs/sharedByOther" + ], + [ + "modules/used-whole.schema.json#/$defs/unused", + "#/$defs/used-whole/$defs/unused" + ], + [ + "modules/used-whole.schema.json#/properties/withDefs/$defs/nestedKept", + "#/$defs/used-whole/properties/withDefs/$defs/nestedKept" ] ] }, "shaken": { - "hollowed": [], - "removed": [] + "hollowed": [ + "#/$defs/main", + "#/$defs/used-array-items", + "#/$defs/used-array-items/properties/choice", + "#/$defs/used-array-items/properties/combined", + "#/$defs/used-properties-only", + "#/$defs/used-properties-only/properties/address", + "#/$defs/used-properties-only/properties/address/properties/geo", + "#/$defs/used-subdefs-only" + ], + "voided": [ + "#/$defs/used-array-items/properties/choice/oneOf/0", + "#/$defs/used-array-items/properties/combined/allOf/0", + "#/$defs/used-array-items/properties/combined/allOf/2" + ], + "removed": [ + "#/$defs/main/$defs/mainUnused", + "#/$defs/main/not", + "#/$defs/unused", + "#/$defs/used-array-items/$defs/droppedDef", + "#/$defs/used-array-items/properties/choice/examples", + "#/$defs/used-array-items/properties/choice/oneOf/1/$defs", + "#/$defs/used-array-items/properties/combined/allOf/4", + "#/$defs/used-array-items/type", + "#/$defs/used-properties-only/$defs/unusedDef", + "#/$defs/used-properties-only/properties/address/properties/city", + "#/$defs/used-properties-only/properties/address/properties/geo/properties/lon", + "#/$defs/used-properties-only/properties/address/properties/geo/type", + "#/$defs/used-properties-only/properties/address/type", + "#/$defs/used-properties-only/properties/unusedTopLevel", + "#/$defs/used-properties-only/type", + "#/$defs/used-subdefs-only/$defs/dropped", + "#/$defs/used-subdefs-only/properties", + "#/$defs/used-subdefs-only/type", + "#/$defs/used-whole-data/$defs", + "#/$defs/used-whole/$defs/unused", + "#/$defs/used-whole/properties/name/$defs", + "#/$defs/used-whole/properties/withDefs/$defs/nestedDropped", + "#/$defs/used-whole/properties/withDefs/anyOf/1/$defs", + "#/$defs/used-whole/properties/withDefs/anyOf/1/if/$defs", + "#/properties/composed/$defs" + ] }, "validationWarnings": [ "Unchecked external ref: externals/not-bundled.schema.json#/$defs/licenseId", From 882fcbcd5adb7918f2577c0daf7e49b93b90a35f Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 26 Oct 2026 11:40:44 +0100 Subject: [PATCH 2/6] chore(bundler): metaSchemaValidation and QA Signed-off-by: Jan Kowalleck --- tools/src/main/js/bundler/src/bundle.js | 7 +++---- .../src/main/js/bundler/src/helpers/json-schema-spec.js | 2 +- .../src/test/js/schema-v2/json-schema-validate-tests.js | 9 +++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tools/src/main/js/bundler/src/bundle.js b/tools/src/main/js/bundler/src/bundle.js index b1142593d..d9366f129 100644 --- a/tools/src/main/js/bundler/src/bundle.js +++ b/tools/src/main/js/bundler/src/bundle.js @@ -212,21 +212,20 @@ export const bundle = async (entryFile, includeFiles, targetFile) => { return { schema, - external: Array.from(externals), + external: Array.from(externals).sort(), embedded: Array.from(embeddedMap, ([f, d]) => { const {path, frag} = refSplit(f); return [ refJoin({path: posixPath(relative(entryDir, path)), frag}), makeLocalRef(d)]; - }), + }).sort(([a], [b]) => a.localeCompare(b)), rewired: Array.from(rewiredMap, ([f, t]) => { const {path, frag} = refSplit(f); return [ refJoin({path: posixPath(relative(entryDir, path)), frag}), t]; - } - ) + }).sort(([a], [b]) => a.localeCompare(b)), }; }; diff --git a/tools/src/main/js/bundler/src/helpers/json-schema-spec.js b/tools/src/main/js/bundler/src/helpers/json-schema-spec.js index 6481fce24..672ac0908 100644 --- a/tools/src/main/js/bundler/src/helpers/json-schema-spec.js +++ b/tools/src/main/js/bundler/src/helpers/json-schema-spec.js @@ -81,7 +81,7 @@ export const subschemas = function* (s) { continue; } if (SCHEMA_MAP.has(k)) { - if (!isSchemaObject(v)) continue; + if (typeof v !== 'object' || v === null) continue; for (const [mk, mv] of Object.entries(v)) { if (isSchemaObject(mv)) yield [mv, [k, mk]]; } diff --git a/tools/src/test/js/schema-v2/json-schema-validate-tests.js b/tools/src/test/js/schema-v2/json-schema-validate-tests.js index aa6f8ee02..d7b3d09ab 100644 --- a/tools/src/test/js/schema-v2/json-schema-validate-tests.js +++ b/tools/src/test/js/schema-v2/json-schema-validate-tests.js @@ -84,6 +84,7 @@ const [spdxSchema, cryptoDefsSchema, behaviorTaxonomySchema, schemas, schemaModu function getAjv(bundled) { // see https://ajv.js.org/options.html const ajv = new Ajv2020({ + allErrors: true, verbose: true, addUsedSchema: false, keywords: ["meta:enum"], @@ -121,17 +122,17 @@ let errCnt = 0 for (const [schemaFile, schema] of schemas) { console.log('\n> SchemaFile: ', schemaFile); - const ajv = getAjv(schemaFilesBundleMatcher.test(basename(schemaFile))) - - console.group(`> compile schema, log warnings ...`) + console.group(`> validate & compile schema, log warnings ...`) try { + const ajv = getAjv(schemaFilesBundleMatcher.test(basename(schemaFile))) + // compile goes beyond validation - it also checs references, and such ajv.compile(schema) } catch (err) { ++errCnt - console.groupEnd() console.error( `!!! SCHEMA ERROR:`, String(err), '\n in file:', schemaFile) + console.groupEnd() continue } console.groupEnd() From 10680257a627f71ce3e183d445848f9067f713ce Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 26 Oct 2026 11:43:05 +0100 Subject: [PATCH 3/6] tests Signed-off-by: Jan Kowalleck --- .../snapshot/main-bundled.min.schema.json | 2 +- .../test/snapshot/main-bundled.schema.json | 212 +++++++++++++++--- .../main/js/bundler/test/snapshot/report.json | 44 +--- 3 files changed, 183 insertions(+), 75 deletions(-) diff --git a/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json b/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json index 1961da8cf..e6530a209 100644 --- a/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json +++ b/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json @@ -1 +1 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"http://example.com/test/main-bundled.min.schema.json","$comment":"FIXTURE: root schema; entry point of the bundle. This $comment is the only one that survives minification. The root title/description must survive in both outputs. The root `required`/`examples` are data arrays inside the (reached) root schema and must survive the tree-shaker untouched.","title":"Main","description":"Root document of the bundler test fixtures.","type":"object","required":["whole"],"examples":[{"whole":{}}],"properties":{"$comment":{"type":"string","title":"Comment"},"whole":{"title":"Whole","description":"A module referenced in its entirety.","$ref":"#/$defs/used-whole"},"wholeData":{"title":"Whole data","description":"A data-heavy module referenced in its entirety.","$ref":"#/$defs/used-whole-data"},"sub":{"title":"Sub","description":"A single definition of another module.","$ref":"#/$defs/used-subdefs-only/$defs/kept"},"local":{"title":"Local","description":"A definition of this very schema.","$ref":"#/$defs/main/$defs/mainLocal"},"composed":{"title":"Composed","description":"An object assembled from parts of another module.","type":"object","properties":{"street":{"title":"Street","description":"The street, taken from the module's address.","$ref":"#/$defs/used-properties-only/properties/address/properties/street"},"latitude":{"title":"Latitude","description":"The latitude, taken from the module's address.geo.","$ref":"#/$defs/used-properties-only/properties/address/properties/geo/properties/lat"},"contact":{"title":"Contact","description":"The contact block of the module, as is.","$ref":"#/$defs/used-properties-only/properties/contact"},"nested":{"title":"Nested","description":"A value defined in a property-level $defs.","$ref":"#/$defs/used-whole/properties/withDefs/$defs/nestedKept"}}},"middle":{"title":"Middle","description":"The middle part of the module's combined schema.","anyOf":[{"$ref":"#/$defs/used-array-items/properties/combined/allOf/1"},{"$ref":"#/$defs/used-array-items/properties/combined/allOf/3"}]},"lastChoice":{"title":"Last choice","description":"The last alternative of the module's choice.","$ref":"#/$defs/used-array-items/properties/choice/oneOf/1"},"anyValue":{"title":"Any value","description":"Accepts anything.","$ref":"#/$defs/used-subdefs-only/$defs/keptEmpty"},"todoValue":{"title":"Todo value","description":"A placeholder.","$ref":"#/$defs/used-subdefs-only/$defs/keptCommentOnly"},"annotatedValue":{"title":"Annotated value","description":"An annotation-only schema.","$ref":"#/$defs/used-subdefs-only/$defs/keptAnnotationsOnly"}},"$defs":{"dynamic-anchor":{"$dynamicAnchor":"node","title":"Dynamic anchor","description":"A recursive tree node, addressable via a dynamic anchor.","type":"object","properties":{"children":{"title":"Children","description":"The child nodes of this node.","type":"array","items":{"title":"Child","description":"A nested node.","$ref":"#/$defs/dynamic-anchor"}}}},"main":{"$defs":{"mainLocal":{"title":"Main local","description":"A local string definition of the root schema.","type":"string"}},"not":{}},"used-array-items":{"title":"Used array items","description":"A module of which only individual array items and a non-schema value are referenced.","properties":{"combined":{"title":"Combined","description":"A composition of which only the middle part is referenced.","allOf":[{"not":{}},{"title":"Combined part 2","description":"The middle part.","type":"object","properties":{"b":{"title":"B","description":"A value defined in this module.","$ref":"#/$defs/used-array-items/$defs/keptViaArray"}}},false,true],"not":{}},"choice":{"title":"Choice","description":"A choice of which only the last alternative is referenced.","oneOf":[{"not":{}},{"title":"Choice 1","description":"The second alternative.","type":"integer"}],"not":{}}},"$defs":{"keptViaArray":{"title":"Kept via array","description":"A boolean value.","type":"boolean"}},"not":{}},"used-properties-only":{"title":"Used properties only","description":"A module of which only nested properties are referenced.","properties":{"address":{"title":"Address","description":"A postal address.","properties":{"street":{"title":"Street","description":"Street name and number.","type":"string"},"geo":{"title":"Geo","description":"Geographic coordinates.","properties":{"lat":{"title":"Latitude","description":"Latitude in degrees.","$ref":"#/$defs/used-properties-only/$defs/degrees"}},"not":{}}},"not":{}},"contact":{"title":"Contact","description":"Contact details.","type":"object","properties":{"email":{"title":"E-mail","description":"An e-mail address.","type":"string","format":"email"},"phone":{"title":"Phone","description":"A phone number.","type":"string"}}}},"$defs":{"degrees":{"title":"Degrees","description":"An angle in degrees.","type":"number","minimum":-180,"maximum":180}},"not":{}},"used-subdefs-only":{"title":"Used sub-defs only","description":"A module of which only individual definitions are referenced.","$defs":{"kept":{"title":"Kept","description":"A definition that is reached directly from the root schema.","type":"object","properties":{"shared":{"title":"Shared","description":"A value defined by another module.","$ref":"#/$defs/used-whole/$defs/sharedByOther"},"fromMain":{"title":"From main","description":"A value defined by the root schema.","$ref":"#/$defs/main/$defs/mainLocal"},"chain":{"title":"Chain","description":"A value defined further down in this module.","$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"}}},"keptEmpty":{},"keptCommentOnly":{},"keptAnnotationsOnly":{"title":"Kept annotations only","description":"A schema that is just an annotation."},"keptTransitively":{"title":"Kept transitively","description":"A string value.","type":"string"}},"not":{}},"used-whole":{"title":"Used whole","description":"A module that is referenced in its entirety.","type":"object","properties":{"name":{"title":"Name","description":"A display name.","type":"string"},"withDefs":{"title":"With defs","description":"A property that hosts its own definitions.","anyOf":[{"title":"Is string","type":"string"},{"title":"Non-zero number","type":"number","if":{"const":0},"then":false,"else":true}],"$defs":{"nestedKept":{"title":"Nested kept","description":"A referenced nested definition.","type":"integer"}}},"inner":{"title":"Inner","description":"Sibling annotations next to a $ref: title/description are kept, while the sibling $comment is removed in the minified output.","$ref":"#/$defs/used-whole/$defs/inner"},"license":{"title":"License","description":"The license of this item.","$ref":"externals/not-bundled.schema.json#/$defs/licenseId"},"remote":{"title":"Remote","description":"A thing defined by a remote schema.","$ref":"https://example.com/remote.schema.json#/$defs/thing"},"inArray":{"title":"In array","description":"A composition of several referenced schemas.","allOf":[{"$ref":"#/$defs/used-whole/$defs/inner"},{"$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"},{"$ref":"externals/not-bundled.schema.json#/$defs/licenseId"}]}},"$defs":{"inner":{"title":"Inner","description":"An integer value.","type":"integer"},"sharedByOther":{"title":"Shared by other","description":"A numeric value shared across modules.","type":"number"}}},"used-whole-data":{"title":"Used whole data","description":"A module referenced in its entirety that carries data-bearing keywords inside reached schemas.","type":"object","required":["status","level"],"examples":[{"status":"on","level":1},{"status":"off","level":2}],"default":{"status":"on","level":1},"properties":{"status":{"title":"Status","description":"A status value.","type":"string","enum":["on","off"],"meta:enum":{"on":"It is on.","off":"It is off."}},"level":{"title":"Level","description":"A fixed level.","type":"integer","const":1,"examples":[1],"default":1},"flags":{"title":"Flags","description":"An object with boolean-schema properties.","type":"object","required":["anything"],"properties":{"anything":true,"nothing":false},"additionalProperties":false},"tags":{"title":"Tags","description":"A list of tags.","type":"array","uniqueItems":true,"minItems":0,"items":{"title":"Tag","description":"A single tag.","type":"string"}}}}}} \ No newline at end of file +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"http://example.com/test/main-bundled.min.schema.json","$comment":"FIXTURE: root schema; entry point of the bundle. This $comment is the only one that survives minification. The root title/description must survive in both outputs. The root `required`/`examples` are data arrays inside the (reached) root schema and must survive the tree-shaker untouched.","title":"Main","description":"Root document of the bundler test fixtures.","type":"object","required":["whole"],"examples":[{"whole":{}}],"properties":{"$comment":{"type":"string","title":"Comment"},"whole":{"title":"Whole","description":"A module referenced in its entirety.","$ref":"#/$defs/used-whole"},"wholeData":{"title":"Whole data","description":"A data-heavy module referenced in its entirety.","$ref":"#/$defs/used-whole-data"},"sub":{"title":"Sub","description":"A single definition of another module.","$ref":"#/$defs/used-subdefs-only/$defs/kept"},"local":{"title":"Local","description":"A definition of this very schema.","$ref":"#/$defs/main/$defs/mainLocal"},"composed":{"title":"Composed","description":"An object assembled from parts of another module.","type":"object","$defs":{"rootNestedDropped":{"title":"Root nested dropped","description":"An unreferenced nested definition of the root.","type":"null"}},"properties":{"street":{"title":"Street","description":"The street, taken from the module's address.","$ref":"#/$defs/used-properties-only/properties/address/properties/street"},"latitude":{"title":"Latitude","description":"The latitude, taken from the module's address.geo.","$ref":"#/$defs/used-properties-only/properties/address/properties/geo/properties/lat"},"contact":{"title":"Contact","description":"The contact block of the module, as is.","$ref":"#/$defs/used-properties-only/properties/contact"},"nested":{"title":"Nested","description":"A value defined in a property-level $defs.","$ref":"#/$defs/used-whole/properties/withDefs/$defs/nestedKept"}}},"middle":{"title":"Middle","description":"The middle part of the module's combined schema.","anyOf":[{"$ref":"#/$defs/used-array-items/properties/combined/allOf/1"},{"$ref":"#/$defs/used-array-items/properties/combined/allOf/3"}]},"lastChoice":{"title":"Last choice","description":"The last alternative of the module's choice.","$ref":"#/$defs/used-array-items/properties/choice/oneOf/1"},"anyValue":{"title":"Any value","description":"Accepts anything.","$ref":"#/$defs/used-subdefs-only/$defs/keptEmpty"},"todoValue":{"title":"Todo value","description":"A placeholder.","$ref":"#/$defs/used-subdefs-only/$defs/keptCommentOnly"},"annotatedValue":{"title":"Annotated value","description":"An annotation-only schema.","$ref":"#/$defs/used-subdefs-only/$defs/keptAnnotationsOnly"}},"$defs":{"dynamic-anchor":{"$dynamicAnchor":"node","title":"Dynamic anchor","description":"A recursive tree node, addressable via a dynamic anchor.","type":"object","properties":{"children":{"title":"Children","description":"The child nodes of this node.","type":"array","items":{"title":"Child","description":"A nested node.","$ref":"#/$defs/dynamic-anchor"}}}},"main":{"$defs":{"mainLocal":{"title":"Main local","description":"A local string definition of the root schema.","type":"string"},"mainUnused":{"title":"Main unused","description":"A definition nothing refers to.","type":"null"}},"not":{}},"unused":{"title":"Unused","description":"A module that nothing reachable refers to.","type":"object","properties":{"pullsNothingIn":{"title":"Pulls nothing in","description":"A flag defined by another module.","$ref":"#/$defs/used-whole/$defs/unused"}}},"used-array-items":{"title":"Used array items","description":"A module of which only individual array items and a non-schema value are referenced.","type":"object","properties":{"combined":{"title":"Combined","description":"A composition of which only the middle part is referenced.","allOf":[{"title":"Combined part 0","description":"The first part.","type":"object","properties":{"a":{"title":"A","description":"An unreferenced value.","type":"string"}}},{"title":"Combined part 2","description":"The middle part.","type":"object","properties":{"b":{"title":"B","description":"A value defined in this module.","$ref":"#/$defs/used-array-items/$defs/keptViaArray"}}},false,true,{"title":"Combined part 6","description":"The end part.","type":"object","properties":{"a":{"title":"A","description":"An unreferenced value.","type":"string"}}}]},"choice":{"title":"Choice","description":"A choice of which only the last alternative is referenced.","oneOf":[{"title":"Choice 0","description":"The first alternative.","type":"null","$defs":{"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}},{"title":"Choice 1","description":"The second alternative.","type":"integer","$defs":{"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}}],"examples":[null,1,2,3]}},"$defs":{"keptViaArray":{"title":"Kept via array","description":"A boolean value.","type":"boolean"},"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}},"used-properties-only":{"title":"Used properties only","description":"A module of which only nested properties are referenced.","type":"object","properties":{"address":{"title":"Address","description":"A postal address.","type":"object","properties":{"street":{"title":"Street","description":"Street name and number.","type":"string"},"city":{"title":"City","description":"Name of the city.","type":"string"},"geo":{"title":"Geo","description":"Geographic coordinates.","type":"object","properties":{"lat":{"title":"Latitude","description":"Latitude in degrees.","$ref":"#/$defs/used-properties-only/$defs/degrees"},"lon":{"title":"Longitude","description":"Longitude in degrees.","$ref":"#/$defs/used-properties-only/$defs/degrees"}}}}},"contact":{"title":"Contact","description":"Contact details.","type":"object","properties":{"email":{"title":"E-mail","description":"An e-mail address.","type":"string","format":"email"},"phone":{"title":"Phone","description":"A phone number.","type":"string"}}},"unusedTopLevel":{"title":"Unused top level","description":"A property nothing refers to.","$ref":"#/$defs/unused"}},"$defs":{"degrees":{"title":"Degrees","description":"An angle in degrees.","type":"number","minimum":-180,"maximum":180},"unusedDef":{"title":"Unused def","description":"A definition nothing reachable refers to.","type":"null"}}},"used-subdefs-only":{"title":"Used sub-defs only","description":"A module of which only individual definitions are referenced.","type":"object","properties":{"body":{"title":"Body","description":"The body of this module.","$ref":"#/$defs/unused"}},"$defs":{"kept":{"title":"Kept","description":"A definition that is reached directly from the root schema.","type":"object","properties":{"shared":{"title":"Shared","description":"A value defined by another module.","$ref":"#/$defs/used-whole/$defs/sharedByOther"},"fromMain":{"title":"From main","description":"A value defined by the root schema.","$ref":"#/$defs/main/$defs/mainLocal"},"chain":{"title":"Chain","description":"A value defined further down in this module.","$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"}}},"keptEmpty":{},"keptCommentOnly":{},"keptAnnotationsOnly":{"title":"Kept annotations only","description":"A schema that is just an annotation."},"keptTransitively":{"title":"Kept transitively","description":"A string value.","type":"string"},"dropped":{"title":"Dropped","description":"A definition nothing reachable refers to.","type":"null"}}},"used-whole":{"title":"Used whole","description":"A module that is referenced in its entirety.","type":"object","properties":{"name":{"title":"Name","description":"A display name.","type":"string","$defs":{"nestedDropped":{"title":"Nested dropped","description":"An unreferenced nested definition.","type":"null"}}},"withDefs":{"title":"With defs","description":"A property that hosts its own definitions.","anyOf":[{"title":"Is string","type":"string"},{"title":"Non-zero number","type":"number","if":{"const":0,"$defs":{"unused":{"title":"Unused","type":"null"}}},"then":false,"else":true,"$defs":{"unusedPositive":{"title":"Number > 0","type":"number","exclusiveMinimum":0},"unusedNegative":{"title":"Number < 0","type":"number","exclusiveMaximum":0}}}],"$defs":{"nestedKept":{"title":"Nested kept","description":"A referenced nested definition.","type":"integer"},"nestedDropped":{"title":"Nested dropped","description":"An unreferenced nested definition.","type":"null"}}},"inner":{"title":"Inner","description":"Sibling annotations next to a $ref: title/description are kept, while the sibling $comment is removed in the minified output.","$ref":"#/$defs/used-whole/$defs/inner"},"license":{"title":"License","description":"The license of this item.","$ref":"externals/not-bundled.schema.json#/$defs/licenseId"},"remote":{"title":"Remote","description":"A thing defined by a remote schema.","$ref":"https://example.com/remote.schema.json#/$defs/thing"},"inArray":{"title":"In array","description":"A composition of several referenced schemas.","allOf":[{"$ref":"#/$defs/used-whole/$defs/inner"},{"$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"},{"$ref":"externals/not-bundled.schema.json#/$defs/licenseId"}]}},"$defs":{"inner":{"title":"Inner","description":"An integer value.","type":"integer"},"unused":{"title":"Unused","description":"A flag.","type":"boolean"},"sharedByOther":{"title":"Shared by other","description":"A numeric value shared across modules.","type":"number"}}},"used-whole-data":{"title":"Used whole data","description":"A module referenced in its entirety that carries data-bearing keywords inside reached schemas.","type":"object","required":["status","level"],"examples":[{"status":"on","level":1},{"status":"off","level":2}],"default":{"status":"on","level":1},"properties":{"status":{"title":"Status","description":"A status value.","type":"string","enum":["on","off"],"meta:enum":{"on":"It is on.","off":"It is off."}},"level":{"title":"Level","description":"A fixed level.","type":"integer","const":1,"examples":[1],"default":1},"flags":{"title":"Flags","description":"An object with boolean-schema properties.","type":"object","required":["anything"],"properties":{"anything":true,"nothing":false},"additionalProperties":false},"tags":{"title":"Tags","description":"A list of tags.","type":"array","uniqueItems":true,"minItems":0,"items":{"title":"Tag","description":"A single tag.","type":"string"}}},"$defs":{"unusedDef":{"title":"Unused def","description":"A definition nothing reachable refers to.","type":"null"}}}}} \ No newline at end of file diff --git a/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json b/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json index e95a7a642..235151154 100644 --- a/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json +++ b/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json @@ -48,6 +48,14 @@ "title": "Composed", "description": "An object assembled from parts of another module.", "type": "object", + "$defs": { + "rootNestedDropped": { + "$comment": "Nested $defs inside a REACHED property of the ROOT schema. Unreferenced -> must be removed; the root and its properties are reached, yet their $defs still need sweeping. The then-empty `$defs` object is removed too.", + "title": "Root nested dropped", + "description": "An unreferenced nested definition of the root.", + "type": "null" + } + }, "properties": { "street": { "$comment": "Ref into a nested property of the module -> #/properties/address/properties/street.", @@ -140,15 +148,35 @@ "title": "Main local", "description": "A local string definition of the root schema.", "type": "string" + }, + "mainUnused": { + "$comment": "Never referenced -> shaken away from #/$defs/main/$defs.", + "title": "Main unused", + "description": "A definition nothing refers to.", + "type": "null" } }, "not": { - "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." + "$comment": "This schema is hollow: it is as a container for definitions. Itself must not be used for validation." + } + }, + "unused": { + "title": "Unused", + "description": "A module that nothing reachable refers to.", + "type": "object", + "properties": { + "pullsNothingIn": { + "$comment": "Even though this refs a module, it is itself unreachable and must not influence shaking.", + "title": "Pulls nothing in", + "description": "A flag defined by another module.", + "$ref": "#/$defs/used-whole/$defs/unused" + } } }, "used-array-items": { "title": "Used array items", "description": "A module of which only individual array items and a non-schema value are referenced.", + "type": "object", "properties": { "combined": { "$comment": "Only allOf/1 and allOf/3 are referenced from main. allOf/0 and allOf/2 are not reached -> they must become void placeholders or stay boolean, never JSON null, so that the index of allOf/3 stays stable; the rest can be truncated. 'combined' itself is not reached -> hollowed.", @@ -156,8 +184,16 @@ "description": "A composition of which only the middle part is referenced.", "allOf": [ { - "not": { - "$comment": "This schema is void: it is not referenced and was removed. It only remains as a placeholder to keep sibling positions stable. Itself must not be used for validation." + "$comment": "allOf/0: NOT referenced -> void placeholder.", + "title": "Combined part 0", + "description": "The first part.", + "type": "object", + "properties": { + "a": { + "title": "A", + "description": "An unreferenced value.", + "type": "string" + } } }, { @@ -174,11 +210,21 @@ } }, false, - true - ], - "not": { - "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." - } + true, + { + "$comment": "allOf/4: NOT referenced ano nothing used after -> removed.", + "title": "Combined part 6", + "description": "The end part.", + "type": "object", + "properties": { + "a": { + "title": "A", + "description": "An unreferenced value.", + "type": "string" + } + } + } + ] }, "choice": { "$comment": "Only the LAST item of oneOf is referenced -> oneOf/0 becomes a void placeholder, oneOf/1 is kept. The examples are not referenced -> remove", @@ -186,20 +232,40 @@ "description": "A choice of which only the last alternative is referenced.", "oneOf": [ { - "not": { - "$comment": "This schema is void: it is not referenced and was removed. It only remains as a placeholder to keep sibling positions stable. Itself must not be used for validation." + "$comment": "oneOf/0: NOT referenced -> void placeholder.", + "title": "Choice 0", + "description": "The first alternative.", + "type": "null", + "$defs": { + "droppedDef": { + "$comment": "Not reachable -> shaken away.", + "title": "Dropped def", + "description": "A definition nothing reachable refers to.", + "type": "null" + } } }, { "$comment": "oneOf/1: referenced from main via #/properties/choice/oneOf/1.", "title": "Choice 1", "description": "The second alternative.", - "type": "integer" + "type": "integer", + "$defs": { + "droppedDef": { + "$comment": "Not reachable -> shaken away.", + "title": "Dropped def", + "description": "A definition nothing reachable refers to.", + "type": "null" + } + } } ], - "not": { - "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." - } + "examples": [ + null, + 1, + 2, + 3 + ] } }, "$defs": { @@ -208,20 +274,25 @@ "title": "Kept via array", "description": "A boolean value.", "type": "boolean" + }, + "droppedDef": { + "$comment": "Not reachable -> shaken away.", + "title": "Dropped def", + "description": "A definition nothing reachable refers to.", + "type": "null" } - }, - "not": { - "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." } }, "used-properties-only": { "title": "Used properties only", "description": "A module of which only nested properties are referenced.", + "type": "object", "properties": { "address": { "$comment": "Reached only through its nested children, never as a whole -> the container stays hollow, only the referenced children survive.", "title": "Address", "description": "A postal address.", + "type": "object", "properties": { "street": { "$comment": "Reached from main via #/properties/address/properties/street.", @@ -229,25 +300,32 @@ "description": "Street name and number.", "type": "string" }, + "city": { + "$comment": "Not referenced -> dropped although its siblings are kept.", + "title": "City", + "description": "Name of the city.", + "type": "string" + }, "geo": { "$comment": "Reached from main via a three-level deep pointer into #/properties/address/properties/geo/properties/lat.", "title": "Geo", "description": "Geographic coordinates.", + "type": "object", "properties": { "lat": { "$comment": "Deeply nested leaf that is referenced directly from main. Has an internal ref -> transitively keeps $defs/degrees.", "title": "Latitude", "description": "Latitude in degrees.", "$ref": "#/$defs/used-properties-only/$defs/degrees" + }, + "lon": { + "$comment": "Deeply nested leaf that is NOT referenced -> dropped.", + "title": "Longitude", + "description": "Longitude in degrees.", + "$ref": "#/$defs/used-properties-only/$defs/degrees" } - }, - "not": { - "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." } } - }, - "not": { - "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." } }, "contact": { @@ -269,6 +347,12 @@ "type": "string" } } + }, + "unusedTopLevel": { + "$comment": "Not referenced -> dropped together with everything it refers to; must NOT make unused.schema.json reachable.", + "title": "Unused top level", + "description": "A property nothing refers to.", + "$ref": "#/$defs/unused" } }, "$defs": { @@ -279,15 +363,27 @@ "type": "number", "minimum": -180, "maximum": 180 + }, + "unusedDef": { + "$comment": "Not reachable -> shaken away.", + "title": "Unused def", + "description": "A definition nothing reachable refers to.", + "type": "null" } - }, - "not": { - "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." } }, "used-subdefs-only": { "title": "Used sub-defs only", "description": "A module of which only individual definitions are referenced.", + "type": "object", + "properties": { + "body": { + "$comment": "Lives in the dropped body, so this ref must NOT make unused.schema.json reachable.", + "title": "Body", + "description": "The body of this module.", + "$ref": "#/$defs/unused" + } + }, "$defs": { "kept": { "$comment": "Reached from main. Exercises three ref flavours.", @@ -329,10 +425,13 @@ "title": "Kept transitively", "description": "A string value.", "type": "string" + }, + "dropped": { + "$comment": "Not reachable -> shaken away.", + "title": "Dropped", + "description": "A definition nothing reachable refers to.", + "type": "null" } - }, - "not": { - "$comment": "This schema is hollow: it is not referenced and only remains as a container for structures that are still in use. Itself must not be used for validation." } }, "used-whole": { @@ -343,7 +442,15 @@ "name": { "title": "Name", "description": "A display name.", - "type": "string" + "type": "string", + "$defs": { + "nestedDropped": { + "$comment": "Lives in $defs of a REACHED property (used-whole is used as a whole). Nothing refs it -> removed; the then-empty `$defs` object is removed too.", + "title": "Nested dropped", + "description": "An unreferenced nested definition.", + "type": "null" + } + } }, "withDefs": { "$comment": "A REACHED property carrying nested $defs of which exactly one is referenced from main.", @@ -359,10 +466,31 @@ "type": "number", "$comment": "if/then/else are intentionally NOT schema objects.", "if": { - "const": 0 + "const": 0, + "$defs": { + "unused": { + "$comment": "unreferenced -> shaken away.", + "title": "Unused", + "type": "null" + } + } }, "then": false, - "else": true + "else": true, + "$defs": { + "unusedPositive": { + "$comment": "unreferenced -> shaken away.", + "title": "Number > 0", + "type": "number", + "exclusiveMinimum": 0 + }, + "unusedNegative": { + "$comment": "unreferenced -> shaken away.", + "title": "Number < 0", + "type": "number", + "exclusiveMaximum": 0 + } + } } ], "$defs": { @@ -371,6 +499,12 @@ "title": "Nested kept", "description": "A referenced nested definition.", "type": "integer" + }, + "nestedDropped": { + "$comment": "Sibling of a kept nested def, itself unreferenced -> removed.", + "title": "Nested dropped", + "description": "An unreferenced nested definition.", + "type": "null" } } }, @@ -415,6 +549,12 @@ "description": "An integer value.", "type": "integer" }, + "unused": { + "$comment": "Not reachable -> shaken away.", + "title": "Unused", + "description": "A flag.", + "type": "boolean" + }, "sharedByOther": { "$comment": "Cross-referenced from used-subdefs-only#/$defs/kept.", "title": "Shared by other", @@ -498,6 +638,14 @@ "type": "string" } } + }, + "$defs": { + "unusedDef": { + "$comment": "The ONLY thing the tree-shaker is allowed to drop from this module.", + "title": "Unused def", + "description": "A definition nothing reachable refers to.", + "type": "null" + } } } } diff --git a/tools/src/main/js/bundler/test/snapshot/report.json b/tools/src/main/js/bundler/test/snapshot/report.json index 681609628..f05b98ee8 100644 --- a/tools/src/main/js/bundler/test/snapshot/report.json +++ b/tools/src/main/js/bundler/test/snapshot/report.json @@ -134,48 +134,8 @@ ] }, "shaken": { - "hollowed": [ - "#/$defs/main", - "#/$defs/used-array-items", - "#/$defs/used-array-items/properties/choice", - "#/$defs/used-array-items/properties/combined", - "#/$defs/used-properties-only", - "#/$defs/used-properties-only/properties/address", - "#/$defs/used-properties-only/properties/address/properties/geo", - "#/$defs/used-subdefs-only" - ], - "voided": [ - "#/$defs/used-array-items/properties/choice/oneOf/0", - "#/$defs/used-array-items/properties/combined/allOf/0", - "#/$defs/used-array-items/properties/combined/allOf/2" - ], - "removed": [ - "#/$defs/main/$defs/mainUnused", - "#/$defs/main/not", - "#/$defs/unused", - "#/$defs/used-array-items/$defs/droppedDef", - "#/$defs/used-array-items/properties/choice/examples", - "#/$defs/used-array-items/properties/choice/oneOf/1/$defs", - "#/$defs/used-array-items/properties/combined/allOf/4", - "#/$defs/used-array-items/type", - "#/$defs/used-properties-only/$defs/unusedDef", - "#/$defs/used-properties-only/properties/address/properties/city", - "#/$defs/used-properties-only/properties/address/properties/geo/properties/lon", - "#/$defs/used-properties-only/properties/address/properties/geo/type", - "#/$defs/used-properties-only/properties/address/type", - "#/$defs/used-properties-only/properties/unusedTopLevel", - "#/$defs/used-properties-only/type", - "#/$defs/used-subdefs-only/$defs/dropped", - "#/$defs/used-subdefs-only/properties", - "#/$defs/used-subdefs-only/type", - "#/$defs/used-whole-data/$defs", - "#/$defs/used-whole/$defs/unused", - "#/$defs/used-whole/properties/name/$defs", - "#/$defs/used-whole/properties/withDefs/$defs/nestedDropped", - "#/$defs/used-whole/properties/withDefs/anyOf/1/$defs", - "#/$defs/used-whole/properties/withDefs/anyOf/1/if/$defs", - "#/properties/composed/$defs" - ] + "hollowed": [], + "removed": [] }, "validationWarnings": [ "Unchecked external ref: externals/not-bundled.schema.json#/$defs/licenseId", From 65806c9b941c961ae5eb901954c496938a500388 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 26 Oct 2026 11:50:44 +0100 Subject: [PATCH 4/6] tests Signed-off-by: Jan Kowalleck --- .../test/fixtures/modules/used-array-items.schema.json | 8 ++++++-- .../js/bundler/test/snapshot/main-bundled.min.schema.json | 2 +- .../js/bundler/test/snapshot/main-bundled.schema.json | 8 ++++++-- tools/src/main/js/bundler/test/snapshot/report.json | 4 ++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/tools/src/main/js/bundler/test/fixtures/modules/used-array-items.schema.json b/tools/src/main/js/bundler/test/fixtures/modules/used-array-items.schema.json index b984b25fb..83ba63612 100644 --- a/tools/src/main/js/bundler/test/fixtures/modules/used-array-items.schema.json +++ b/tools/src/main/js/bundler/test/fixtures/modules/used-array-items.schema.json @@ -26,7 +26,7 @@ }, { "$comment": "allOf/1: referenced from main via #/properties/combined/allOf/1 -> kept as a whole, including its internal ref to $defs/keptViaArray.", - "title": "Combined part 2", + "title": "Combined part 1", "description": "The middle part.", "type": "object", "properties": { @@ -40,7 +40,7 @@ false, true, { - "$comment": "allOf/4: NOT referenced ano nothing used after -> removed.", + "$comment": "allOf/4: NOT referenced and nothing used after it -> removed.", "title": "Combined part 6", "description": "The end part.", "type": "object", @@ -51,6 +51,10 @@ "type": "string" } } + }, + { + "$comment": "allOf/5: NOT referenced and nothing used after it -> removed.", + "$ref": "#/$defs/droppedDef" } ] }, diff --git a/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json b/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json index e6530a209..cd6514af3 100644 --- a/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json +++ b/tools/src/main/js/bundler/test/snapshot/main-bundled.min.schema.json @@ -1 +1 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"http://example.com/test/main-bundled.min.schema.json","$comment":"FIXTURE: root schema; entry point of the bundle. This $comment is the only one that survives minification. The root title/description must survive in both outputs. The root `required`/`examples` are data arrays inside the (reached) root schema and must survive the tree-shaker untouched.","title":"Main","description":"Root document of the bundler test fixtures.","type":"object","required":["whole"],"examples":[{"whole":{}}],"properties":{"$comment":{"type":"string","title":"Comment"},"whole":{"title":"Whole","description":"A module referenced in its entirety.","$ref":"#/$defs/used-whole"},"wholeData":{"title":"Whole data","description":"A data-heavy module referenced in its entirety.","$ref":"#/$defs/used-whole-data"},"sub":{"title":"Sub","description":"A single definition of another module.","$ref":"#/$defs/used-subdefs-only/$defs/kept"},"local":{"title":"Local","description":"A definition of this very schema.","$ref":"#/$defs/main/$defs/mainLocal"},"composed":{"title":"Composed","description":"An object assembled from parts of another module.","type":"object","$defs":{"rootNestedDropped":{"title":"Root nested dropped","description":"An unreferenced nested definition of the root.","type":"null"}},"properties":{"street":{"title":"Street","description":"The street, taken from the module's address.","$ref":"#/$defs/used-properties-only/properties/address/properties/street"},"latitude":{"title":"Latitude","description":"The latitude, taken from the module's address.geo.","$ref":"#/$defs/used-properties-only/properties/address/properties/geo/properties/lat"},"contact":{"title":"Contact","description":"The contact block of the module, as is.","$ref":"#/$defs/used-properties-only/properties/contact"},"nested":{"title":"Nested","description":"A value defined in a property-level $defs.","$ref":"#/$defs/used-whole/properties/withDefs/$defs/nestedKept"}}},"middle":{"title":"Middle","description":"The middle part of the module's combined schema.","anyOf":[{"$ref":"#/$defs/used-array-items/properties/combined/allOf/1"},{"$ref":"#/$defs/used-array-items/properties/combined/allOf/3"}]},"lastChoice":{"title":"Last choice","description":"The last alternative of the module's choice.","$ref":"#/$defs/used-array-items/properties/choice/oneOf/1"},"anyValue":{"title":"Any value","description":"Accepts anything.","$ref":"#/$defs/used-subdefs-only/$defs/keptEmpty"},"todoValue":{"title":"Todo value","description":"A placeholder.","$ref":"#/$defs/used-subdefs-only/$defs/keptCommentOnly"},"annotatedValue":{"title":"Annotated value","description":"An annotation-only schema.","$ref":"#/$defs/used-subdefs-only/$defs/keptAnnotationsOnly"}},"$defs":{"dynamic-anchor":{"$dynamicAnchor":"node","title":"Dynamic anchor","description":"A recursive tree node, addressable via a dynamic anchor.","type":"object","properties":{"children":{"title":"Children","description":"The child nodes of this node.","type":"array","items":{"title":"Child","description":"A nested node.","$ref":"#/$defs/dynamic-anchor"}}}},"main":{"$defs":{"mainLocal":{"title":"Main local","description":"A local string definition of the root schema.","type":"string"},"mainUnused":{"title":"Main unused","description":"A definition nothing refers to.","type":"null"}},"not":{}},"unused":{"title":"Unused","description":"A module that nothing reachable refers to.","type":"object","properties":{"pullsNothingIn":{"title":"Pulls nothing in","description":"A flag defined by another module.","$ref":"#/$defs/used-whole/$defs/unused"}}},"used-array-items":{"title":"Used array items","description":"A module of which only individual array items and a non-schema value are referenced.","type":"object","properties":{"combined":{"title":"Combined","description":"A composition of which only the middle part is referenced.","allOf":[{"title":"Combined part 0","description":"The first part.","type":"object","properties":{"a":{"title":"A","description":"An unreferenced value.","type":"string"}}},{"title":"Combined part 2","description":"The middle part.","type":"object","properties":{"b":{"title":"B","description":"A value defined in this module.","$ref":"#/$defs/used-array-items/$defs/keptViaArray"}}},false,true,{"title":"Combined part 6","description":"The end part.","type":"object","properties":{"a":{"title":"A","description":"An unreferenced value.","type":"string"}}}]},"choice":{"title":"Choice","description":"A choice of which only the last alternative is referenced.","oneOf":[{"title":"Choice 0","description":"The first alternative.","type":"null","$defs":{"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}},{"title":"Choice 1","description":"The second alternative.","type":"integer","$defs":{"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}}],"examples":[null,1,2,3]}},"$defs":{"keptViaArray":{"title":"Kept via array","description":"A boolean value.","type":"boolean"},"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}},"used-properties-only":{"title":"Used properties only","description":"A module of which only nested properties are referenced.","type":"object","properties":{"address":{"title":"Address","description":"A postal address.","type":"object","properties":{"street":{"title":"Street","description":"Street name and number.","type":"string"},"city":{"title":"City","description":"Name of the city.","type":"string"},"geo":{"title":"Geo","description":"Geographic coordinates.","type":"object","properties":{"lat":{"title":"Latitude","description":"Latitude in degrees.","$ref":"#/$defs/used-properties-only/$defs/degrees"},"lon":{"title":"Longitude","description":"Longitude in degrees.","$ref":"#/$defs/used-properties-only/$defs/degrees"}}}}},"contact":{"title":"Contact","description":"Contact details.","type":"object","properties":{"email":{"title":"E-mail","description":"An e-mail address.","type":"string","format":"email"},"phone":{"title":"Phone","description":"A phone number.","type":"string"}}},"unusedTopLevel":{"title":"Unused top level","description":"A property nothing refers to.","$ref":"#/$defs/unused"}},"$defs":{"degrees":{"title":"Degrees","description":"An angle in degrees.","type":"number","minimum":-180,"maximum":180},"unusedDef":{"title":"Unused def","description":"A definition nothing reachable refers to.","type":"null"}}},"used-subdefs-only":{"title":"Used sub-defs only","description":"A module of which only individual definitions are referenced.","type":"object","properties":{"body":{"title":"Body","description":"The body of this module.","$ref":"#/$defs/unused"}},"$defs":{"kept":{"title":"Kept","description":"A definition that is reached directly from the root schema.","type":"object","properties":{"shared":{"title":"Shared","description":"A value defined by another module.","$ref":"#/$defs/used-whole/$defs/sharedByOther"},"fromMain":{"title":"From main","description":"A value defined by the root schema.","$ref":"#/$defs/main/$defs/mainLocal"},"chain":{"title":"Chain","description":"A value defined further down in this module.","$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"}}},"keptEmpty":{},"keptCommentOnly":{},"keptAnnotationsOnly":{"title":"Kept annotations only","description":"A schema that is just an annotation."},"keptTransitively":{"title":"Kept transitively","description":"A string value.","type":"string"},"dropped":{"title":"Dropped","description":"A definition nothing reachable refers to.","type":"null"}}},"used-whole":{"title":"Used whole","description":"A module that is referenced in its entirety.","type":"object","properties":{"name":{"title":"Name","description":"A display name.","type":"string","$defs":{"nestedDropped":{"title":"Nested dropped","description":"An unreferenced nested definition.","type":"null"}}},"withDefs":{"title":"With defs","description":"A property that hosts its own definitions.","anyOf":[{"title":"Is string","type":"string"},{"title":"Non-zero number","type":"number","if":{"const":0,"$defs":{"unused":{"title":"Unused","type":"null"}}},"then":false,"else":true,"$defs":{"unusedPositive":{"title":"Number > 0","type":"number","exclusiveMinimum":0},"unusedNegative":{"title":"Number < 0","type":"number","exclusiveMaximum":0}}}],"$defs":{"nestedKept":{"title":"Nested kept","description":"A referenced nested definition.","type":"integer"},"nestedDropped":{"title":"Nested dropped","description":"An unreferenced nested definition.","type":"null"}}},"inner":{"title":"Inner","description":"Sibling annotations next to a $ref: title/description are kept, while the sibling $comment is removed in the minified output.","$ref":"#/$defs/used-whole/$defs/inner"},"license":{"title":"License","description":"The license of this item.","$ref":"externals/not-bundled.schema.json#/$defs/licenseId"},"remote":{"title":"Remote","description":"A thing defined by a remote schema.","$ref":"https://example.com/remote.schema.json#/$defs/thing"},"inArray":{"title":"In array","description":"A composition of several referenced schemas.","allOf":[{"$ref":"#/$defs/used-whole/$defs/inner"},{"$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"},{"$ref":"externals/not-bundled.schema.json#/$defs/licenseId"}]}},"$defs":{"inner":{"title":"Inner","description":"An integer value.","type":"integer"},"unused":{"title":"Unused","description":"A flag.","type":"boolean"},"sharedByOther":{"title":"Shared by other","description":"A numeric value shared across modules.","type":"number"}}},"used-whole-data":{"title":"Used whole data","description":"A module referenced in its entirety that carries data-bearing keywords inside reached schemas.","type":"object","required":["status","level"],"examples":[{"status":"on","level":1},{"status":"off","level":2}],"default":{"status":"on","level":1},"properties":{"status":{"title":"Status","description":"A status value.","type":"string","enum":["on","off"],"meta:enum":{"on":"It is on.","off":"It is off."}},"level":{"title":"Level","description":"A fixed level.","type":"integer","const":1,"examples":[1],"default":1},"flags":{"title":"Flags","description":"An object with boolean-schema properties.","type":"object","required":["anything"],"properties":{"anything":true,"nothing":false},"additionalProperties":false},"tags":{"title":"Tags","description":"A list of tags.","type":"array","uniqueItems":true,"minItems":0,"items":{"title":"Tag","description":"A single tag.","type":"string"}}},"$defs":{"unusedDef":{"title":"Unused def","description":"A definition nothing reachable refers to.","type":"null"}}}}} \ No newline at end of file +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"http://example.com/test/main-bundled.min.schema.json","$comment":"FIXTURE: root schema; entry point of the bundle. This $comment is the only one that survives minification. The root title/description must survive in both outputs. The root `required`/`examples` are data arrays inside the (reached) root schema and must survive the tree-shaker untouched.","title":"Main","description":"Root document of the bundler test fixtures.","type":"object","required":["whole"],"examples":[{"whole":{}}],"properties":{"$comment":{"type":"string","title":"Comment"},"whole":{"title":"Whole","description":"A module referenced in its entirety.","$ref":"#/$defs/used-whole"},"wholeData":{"title":"Whole data","description":"A data-heavy module referenced in its entirety.","$ref":"#/$defs/used-whole-data"},"sub":{"title":"Sub","description":"A single definition of another module.","$ref":"#/$defs/used-subdefs-only/$defs/kept"},"local":{"title":"Local","description":"A definition of this very schema.","$ref":"#/$defs/main/$defs/mainLocal"},"composed":{"title":"Composed","description":"An object assembled from parts of another module.","type":"object","$defs":{"rootNestedDropped":{"title":"Root nested dropped","description":"An unreferenced nested definition of the root.","type":"null"}},"properties":{"street":{"title":"Street","description":"The street, taken from the module's address.","$ref":"#/$defs/used-properties-only/properties/address/properties/street"},"latitude":{"title":"Latitude","description":"The latitude, taken from the module's address.geo.","$ref":"#/$defs/used-properties-only/properties/address/properties/geo/properties/lat"},"contact":{"title":"Contact","description":"The contact block of the module, as is.","$ref":"#/$defs/used-properties-only/properties/contact"},"nested":{"title":"Nested","description":"A value defined in a property-level $defs.","$ref":"#/$defs/used-whole/properties/withDefs/$defs/nestedKept"}}},"middle":{"title":"Middle","description":"The middle part of the module's combined schema.","anyOf":[{"$ref":"#/$defs/used-array-items/properties/combined/allOf/1"},{"$ref":"#/$defs/used-array-items/properties/combined/allOf/3"}]},"lastChoice":{"title":"Last choice","description":"The last alternative of the module's choice.","$ref":"#/$defs/used-array-items/properties/choice/oneOf/1"},"anyValue":{"title":"Any value","description":"Accepts anything.","$ref":"#/$defs/used-subdefs-only/$defs/keptEmpty"},"todoValue":{"title":"Todo value","description":"A placeholder.","$ref":"#/$defs/used-subdefs-only/$defs/keptCommentOnly"},"annotatedValue":{"title":"Annotated value","description":"An annotation-only schema.","$ref":"#/$defs/used-subdefs-only/$defs/keptAnnotationsOnly"}},"$defs":{"dynamic-anchor":{"$dynamicAnchor":"node","title":"Dynamic anchor","description":"A recursive tree node, addressable via a dynamic anchor.","type":"object","properties":{"children":{"title":"Children","description":"The child nodes of this node.","type":"array","items":{"title":"Child","description":"A nested node.","$ref":"#/$defs/dynamic-anchor"}}}},"main":{"$defs":{"mainLocal":{"title":"Main local","description":"A local string definition of the root schema.","type":"string"},"mainUnused":{"title":"Main unused","description":"A definition nothing refers to.","type":"null"}},"not":{}},"unused":{"title":"Unused","description":"A module that nothing reachable refers to.","type":"object","properties":{"pullsNothingIn":{"title":"Pulls nothing in","description":"A flag defined by another module.","$ref":"#/$defs/used-whole/$defs/unused"}}},"used-array-items":{"title":"Used array items","description":"A module of which only individual array items and a non-schema value are referenced.","type":"object","properties":{"combined":{"title":"Combined","description":"A composition of which only the middle part is referenced.","allOf":[{"title":"Combined part 0","description":"The first part.","type":"object","properties":{"a":{"title":"A","description":"An unreferenced value.","type":"string"}}},{"title":"Combined part 1","description":"The middle part.","type":"object","properties":{"b":{"title":"B","description":"A value defined in this module.","$ref":"#/$defs/used-array-items/$defs/keptViaArray"}}},false,true,{"title":"Combined part 6","description":"The end part.","type":"object","properties":{"a":{"title":"A","description":"An unreferenced value.","type":"string"}}},{"$ref":"#/$defs/used-array-items/$defs/droppedDef"}]},"choice":{"title":"Choice","description":"A choice of which only the last alternative is referenced.","oneOf":[{"title":"Choice 0","description":"The first alternative.","type":"null","$defs":{"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}},{"title":"Choice 1","description":"The second alternative.","type":"integer","$defs":{"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}}],"examples":[null,1,2,3]}},"$defs":{"keptViaArray":{"title":"Kept via array","description":"A boolean value.","type":"boolean"},"droppedDef":{"title":"Dropped def","description":"A definition nothing reachable refers to.","type":"null"}}},"used-properties-only":{"title":"Used properties only","description":"A module of which only nested properties are referenced.","type":"object","properties":{"address":{"title":"Address","description":"A postal address.","type":"object","properties":{"street":{"title":"Street","description":"Street name and number.","type":"string"},"city":{"title":"City","description":"Name of the city.","type":"string"},"geo":{"title":"Geo","description":"Geographic coordinates.","type":"object","properties":{"lat":{"title":"Latitude","description":"Latitude in degrees.","$ref":"#/$defs/used-properties-only/$defs/degrees"},"lon":{"title":"Longitude","description":"Longitude in degrees.","$ref":"#/$defs/used-properties-only/$defs/degrees"}}}}},"contact":{"title":"Contact","description":"Contact details.","type":"object","properties":{"email":{"title":"E-mail","description":"An e-mail address.","type":"string","format":"email"},"phone":{"title":"Phone","description":"A phone number.","type":"string"}}},"unusedTopLevel":{"title":"Unused top level","description":"A property nothing refers to.","$ref":"#/$defs/unused"}},"$defs":{"degrees":{"title":"Degrees","description":"An angle in degrees.","type":"number","minimum":-180,"maximum":180},"unusedDef":{"title":"Unused def","description":"A definition nothing reachable refers to.","type":"null"}}},"used-subdefs-only":{"title":"Used sub-defs only","description":"A module of which only individual definitions are referenced.","type":"object","properties":{"body":{"title":"Body","description":"The body of this module.","$ref":"#/$defs/unused"}},"$defs":{"kept":{"title":"Kept","description":"A definition that is reached directly from the root schema.","type":"object","properties":{"shared":{"title":"Shared","description":"A value defined by another module.","$ref":"#/$defs/used-whole/$defs/sharedByOther"},"fromMain":{"title":"From main","description":"A value defined by the root schema.","$ref":"#/$defs/main/$defs/mainLocal"},"chain":{"title":"Chain","description":"A value defined further down in this module.","$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"}}},"keptEmpty":{},"keptCommentOnly":{},"keptAnnotationsOnly":{"title":"Kept annotations only","description":"A schema that is just an annotation."},"keptTransitively":{"title":"Kept transitively","description":"A string value.","type":"string"},"dropped":{"title":"Dropped","description":"A definition nothing reachable refers to.","type":"null"}}},"used-whole":{"title":"Used whole","description":"A module that is referenced in its entirety.","type":"object","properties":{"name":{"title":"Name","description":"A display name.","type":"string","$defs":{"nestedDropped":{"title":"Nested dropped","description":"An unreferenced nested definition.","type":"null"}}},"withDefs":{"title":"With defs","description":"A property that hosts its own definitions.","anyOf":[{"title":"Is string","type":"string"},{"title":"Non-zero number","type":"number","if":{"const":0,"$defs":{"unused":{"title":"Unused","type":"null"}}},"then":false,"else":true,"$defs":{"unusedPositive":{"title":"Number > 0","type":"number","exclusiveMinimum":0},"unusedNegative":{"title":"Number < 0","type":"number","exclusiveMaximum":0}}}],"$defs":{"nestedKept":{"title":"Nested kept","description":"A referenced nested definition.","type":"integer"},"nestedDropped":{"title":"Nested dropped","description":"An unreferenced nested definition.","type":"null"}}},"inner":{"title":"Inner","description":"Sibling annotations next to a $ref: title/description are kept, while the sibling $comment is removed in the minified output.","$ref":"#/$defs/used-whole/$defs/inner"},"license":{"title":"License","description":"The license of this item.","$ref":"externals/not-bundled.schema.json#/$defs/licenseId"},"remote":{"title":"Remote","description":"A thing defined by a remote schema.","$ref":"https://example.com/remote.schema.json#/$defs/thing"},"inArray":{"title":"In array","description":"A composition of several referenced schemas.","allOf":[{"$ref":"#/$defs/used-whole/$defs/inner"},{"$ref":"#/$defs/used-subdefs-only/$defs/keptTransitively"},{"$ref":"externals/not-bundled.schema.json#/$defs/licenseId"}]}},"$defs":{"inner":{"title":"Inner","description":"An integer value.","type":"integer"},"unused":{"title":"Unused","description":"A flag.","type":"boolean"},"sharedByOther":{"title":"Shared by other","description":"A numeric value shared across modules.","type":"number"}}},"used-whole-data":{"title":"Used whole data","description":"A module referenced in its entirety that carries data-bearing keywords inside reached schemas.","type":"object","required":["status","level"],"examples":[{"status":"on","level":1},{"status":"off","level":2}],"default":{"status":"on","level":1},"properties":{"status":{"title":"Status","description":"A status value.","type":"string","enum":["on","off"],"meta:enum":{"on":"It is on.","off":"It is off."}},"level":{"title":"Level","description":"A fixed level.","type":"integer","const":1,"examples":[1],"default":1},"flags":{"title":"Flags","description":"An object with boolean-schema properties.","type":"object","required":["anything"],"properties":{"anything":true,"nothing":false},"additionalProperties":false},"tags":{"title":"Tags","description":"A list of tags.","type":"array","uniqueItems":true,"minItems":0,"items":{"title":"Tag","description":"A single tag.","type":"string"}}},"$defs":{"unusedDef":{"title":"Unused def","description":"A definition nothing reachable refers to.","type":"null"}}}}} \ No newline at end of file diff --git a/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json b/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json index 235151154..5b2b34c3c 100644 --- a/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json +++ b/tools/src/main/js/bundler/test/snapshot/main-bundled.schema.json @@ -198,7 +198,7 @@ }, { "$comment": "allOf/1: referenced from main via #/properties/combined/allOf/1 -> kept as a whole, including its internal ref to $defs/keptViaArray.", - "title": "Combined part 2", + "title": "Combined part 1", "description": "The middle part.", "type": "object", "properties": { @@ -212,7 +212,7 @@ false, true, { - "$comment": "allOf/4: NOT referenced ano nothing used after -> removed.", + "$comment": "allOf/4: NOT referenced and nothing used after it -> removed.", "title": "Combined part 6", "description": "The end part.", "type": "object", @@ -223,6 +223,10 @@ "type": "string" } } + }, + { + "$comment": "allOf/5: NOT referenced and nothing used after it -> removed.", + "$ref": "#/$defs/used-array-items/$defs/droppedDef" } ] }, diff --git a/tools/src/main/js/bundler/test/snapshot/report.json b/tools/src/main/js/bundler/test/snapshot/report.json index f05b98ee8..e07b8b94a 100644 --- a/tools/src/main/js/bundler/test/snapshot/report.json +++ b/tools/src/main/js/bundler/test/snapshot/report.json @@ -55,6 +55,10 @@ "modules/unused.schema.json", "#/$defs/unused" ], + [ + "modules/used-array-items.schema.json#/$defs/droppedDef", + "#/$defs/used-array-items/$defs/droppedDef" + ], [ "modules/used-array-items.schema.json#/$defs/keptViaArray", "#/$defs/used-array-items/$defs/keptViaArray" From c491cf07d01d6f857ac0d8303fb5a28bb792fcb1 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 26 Oct 2026 12:01:08 +0100 Subject: [PATCH 5/6] docs Signed-off-by: Jan Kowalleck --- tools/src/main/js/bundler/README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tools/src/main/js/bundler/README.md b/tools/src/main/js/bundler/README.md index 750ae43b3..f1a7544c7 100644 --- a/tools/src/main/js/bundler/README.md +++ b/tools/src/main/js/bundler/README.md @@ -12,7 +12,13 @@ into `$defs` instead of via embedded resources. ## Requirements * Node.js `>= 24.2` (uses `import.meta.main`, `RegExp.escape`, `fs.glob`) -* No runtime dependencies +* [`ajv`](https://ajv.js.org/) `^8.20` — used to validate the bundle against the JSON Schema meta schema + +Install the dependencies once: + +```shell +npm ci +``` ## Usage @@ -59,8 +65,11 @@ The bundler is split into stages (one module each under `src/`): * 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.* -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. +3. **Sanity check** (`src/sanity.js`) — validates the bundled result: + * every local JSON-Pointer ref must resolve in the bundle. + Unresolvable refs are errors; external refs and anchor-based refs are reported as warnings only. + * the bundle must be valid against the JSON Schema draft 2020-12 meta schema (checked via `ajv`). + Every meta-schema violation is an error. 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. From 277556ade9a1c779f7a5b661aa8d15c3c0e0a326 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 26 Oct 2026 12:02:56 +0100 Subject: [PATCH 6/6] docs Signed-off-by: Jan Kowalleck --- tools/src/main/js/bundler/test/README.md | 33 ++++++++++++++++-------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/tools/src/main/js/bundler/test/README.md b/tools/src/main/js/bundler/test/README.md index 03d7af090..d693bd59d 100644 --- a/tools/src/main/js/bundler/test/README.md +++ b/tools/src/main/js/bundler/test/README.md @@ -21,6 +21,10 @@ The runner copies `fixtures/` to a temporary directory, runs the CLI's `main()` `main.schema.json` as the entry and `modules/` as the modules directory, writes the returned report to `report.json`, and compares all three generated files byte-for-byte against `snapshot/`. +The bundler validates its output against the JSON Schema draft 2020-12 meta schema and fails on +any violation, so every fixture (and the bundled result of it) must be valid draft 2020-12 — +boolean subschemas (`true`/`false`) included. + Each module file name states the edge case it covers; `externals/not-bundled` covers a file outside the modules directory, the `remote` property in `used-whole` covers an absolute `https://` ref, and `inArray` in `used-whole` covers refs nested inside an array (`allOf`). @@ -28,7 +32,10 @@ the modules directory, the `remote` property in `used-whole` covers an absolute The modules exercise the tree-shaker as follows: - `used-whole` — referenced as a whole from the entry; the module body survives untouched, but - its unreferenced `$defs/unusedButKept` is dropped nevertheless. + its unreferenced `$defs/unused` is dropped nevertheless. +- `used-whole-data` — referenced as a whole; its reached schemas carry data-bearing keywords + (`required`, `examples`, `default`, `enum`, `const`, `meta:enum`) and boolean-schema properties. + All of that must pass the sweep untouched; only `$defs/unusedDef` is dropped. - `used-subdefs-only` — only individual `$defs` are referenced; the module body (`type`, `properties`) is dropped, the module is hollowed, and `$defs/dropped` is removed. A ref from the dropped body to `unused` must *not* make `unused` reachable. @@ -38,12 +45,13 @@ The modules exercise the tree-shaker as follows: (`address`, `geo`, the module itself) are hollowed; unreferenced siblings (`city`, `lon`, `unusedTopLevel`) and `$defs/unusedDef` are dropped. - `used-array-items` — refs point *into* schema arrays (`#/properties/combined/allOf/1`, - `#/properties/choice/oneOf/1`) and to a *non-schema* value (`#/properties/status/enum`). - Non-reached array siblings (`allOf/0`, `allOf/2`, `oneOf/0`) must be replaced by a void - `{"not": …}` placeholder — never removed, so indices and pointers stay stable, and never - serialised as `null`. The non-schema target is kept as a whole and its parent `status` is - hollowed. `$defs/keptViaArray` is kept transitively via `allOf/1`; `$defs/droppedDef` is removed. - A ref from the void `allOf/2` to `unused` must *not* make `unused` reachable. + `#/properties/combined/allOf/3`, `#/properties/choice/oneOf/1`). `allOf/3` is the boolean + schema `true`. Non-reached array items *before* the last reached one (`allOf/0`, `oneOf/0`) must be + replaced by a void `{"not": …}` placeholder — never removed, so indices and pointers stay stable, + and never serialised as `null`; boolean items (`allOf/2` = `false`) stay as they are. Non-reached + items *after* the last reached one (`allOf/4`) may be truncated. The unreferenced `examples` of + `choice` are removed. `$defs/keptViaArray` is kept transitively via `allOf/1`; `$defs/droppedDef` + and the nested `oneOf/*/$defs/droppedDef` are removed. - `unused` — nothing reachable refers to it; the whole module is removed. - `dynamic-anchor` — declares a `$dynamicAnchor`; the tree-shaker cannot follow anchors and keeps it as is. @@ -56,9 +64,11 @@ The entry schema additionally covers: `$defs/mainUnused` is dropped from that container. - an inline object property (`properties.composed`) whose own properties reference *properties* of a module rather than its `$defs` (see `used-properties-only` above). -- refs into *array items* and to a *non-schema value* of a module - (`properties.middle`, `properties.lastChoice`, `properties.statusValues`; +- refs into *array items* of a module (`properties.middle`, `properties.lastChoice`; see `used-array-items` above). +- refs to an empty `{}`, a `$comment`-only and an annotation-only schema + (`properties.anyValue`, `properties.todoValue`, `properties.annotatedValue`) — reached, so + they must survive and must *not* be hollowed. Inside the fixtures, the annotation keywords have distinct roles: @@ -72,8 +82,9 @@ Inside the fixtures, the annotation keywords have distinct roles: `report.json` pins the bundler's bookkeeping: which files were embedded and under which `$defs` key, which refs were left external, every rewired ref (source → target), the tree-shake result (every `reached`, `hollowed` and `removed` JSON pointer), and the -validation warnings. Paths in the report are relative to the entry schema's directory, -so the snapshot is independent of the temporary work directory. +validation warnings. Paths in the report are relative to the entry schema's directory and +the `embedded`/`external`/`rewired` lists are sorted, so the snapshot is independent of the +temporary work directory and of file-system iteration order. ## Run