Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 24 additions & 11 deletions tools/src/main/js/bundler/src/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import {
ID_KEYWORD, NOT_KEYWORD, SCHEMA_KEYWORD, COMMENT_KEYWORD, DEFS_KEYWORD, REF_KEYWORDS,
isSchemaObject, subschemas,
refIsAbsolute, refSplit, refJoin, REF_FRAG_SEP
refIsAbsolute, refSplit, refJoin, makeLocalRef
} from './helpers/json-schema-spec.js';
import {PATH_SEP as URI_PATH_SEP} from './helpers/uri.js';

Expand All @@ -19,7 +19,9 @@ const HOLLOW_COMMENT =
'Itself must not be used for validation.';


const FRAG_DEFS_PREFIX = `${JP_PREFIX}${DEFS_KEYWORD}${JP_PREFIX}`;
const JP_DEFS = `${JP_PREFIX}${DEFS_KEYWORD}`;
const JP_DEFS_PREFIX = `${JP_DEFS}${JP_PREFIX}`;


/**
* @param {string} f
Expand Down Expand Up @@ -50,13 +52,17 @@ class ModuleNameCollisionError extends Error {
}
}

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

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

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

/**
* @typedef {object} BundleResult
* @property {Schema} schema
* @property {string[]} embedded embedded schema files
* @property {Array<[string, string]>} external external refs
* @property {string[]} external external refs
* @property {Array<[string, LocalRef]>} embedded embedded schema files
* @property {Array<[string, string]>} rewired rewired refs
*/

Expand Down Expand Up @@ -97,7 +103,7 @@ export const bundle = async (entryFile, includeFiles, targetFile) => {
/**
* Keys are source schema ref with absolute fs path.
* Values are target schema local ref.
* @type {Map<string, string>}
* @type {Map<string, JsonPointer>}
*/
const embeddedMap = new Map();
/**
Expand Down Expand Up @@ -144,9 +150,9 @@ export const bundle = async (entryFile, includeFiles, targetFile) => {
}
rewiredRS = {
path: null,
frag: (refFile === entryFile && !frag?.startsWith(FRAG_DEFS_PREFIX)
frag: (refFile === entryFile && !frag?.startsWith(JP_DEFS_PREFIX)
? ROOT_POINTER
: `${FRAG_DEFS_PREFIX}${escapeJsonPointer(moduleName)}`
: `${JP_DEFS_PREFIX}${escapeJsonPointer(moduleName)}`
) + (frag ?? '')
};
} else {
Expand Down Expand Up @@ -184,7 +190,10 @@ export const bundle = async (entryFile, includeFiles, targetFile) => {
[DEFS_KEYWORD]: schemaDefsOrig,
[NOT_KEYWORD]: {[COMMENT_KEYWORD]: HOLLOW_COMMENT}
};
embeddedMap.set(`${entryFile}${REF_FRAG_SEP}${JP_PREFIX}${DEFS_KEYWORD}`, `${REF_FRAG_SEP}${FRAG_DEFS_PREFIX}${escapeJsonPointer(entryModuleName)}`);
embeddedMap.set(refJoin({
path: entryFile,
frag: JP_DEFS
}), `${JP_DEFS_PREFIX}${escapeJsonPointer(entryModuleName)}`);
} else {
delete schema[DEFS_KEYWORD][entryModuleName];
}
Expand All @@ -198,7 +207,7 @@ export const bundle = async (entryFile, includeFiles, targetFile) => {
delete includeSchema[COMMENT_KEYWORD];
rewireRefs(includeSchema, includeFile);
schema[DEFS_KEYWORD][moduleName] = includeSchema;
embeddedMap.set(includeFile, `${REF_FRAG_SEP}${FRAG_DEFS_PREFIX}${escapeJsonPointer(moduleName)}`);
embeddedMap.set(includeFile, `${JP_DEFS_PREFIX}${escapeJsonPointer(moduleName)}`);
}

return {
Expand All @@ -207,12 +216,16 @@ export const bundle = async (entryFile, includeFiles, targetFile) => {
embedded: Array.from(embeddedMap,
([f, d]) => {
const {path, frag} = refSplit(f);
return [refJoin({path: posixPath(relative(entryDir, path)), frag}), d];
return [
refJoin({path: posixPath(relative(entryDir, path)), frag}),
makeLocalRef(d)];
}),
rewired: Array.from(rewiredMap,
([f, t]) => {
const {path, frag} = refSplit(f);
return [refJoin({path: posixPath(relative(entryDir, path)), frag}), t];
return [
refJoin({path: posixPath(relative(entryDir, path)), frag}),
t];
}
)
};
Expand Down
34 changes: 27 additions & 7 deletions tools/src/main/js/bundler/src/helpers/json-schema-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
* Deprecated keywords are ignored.
*/


/**
* Json Schema
* @typedef {Record<string, unknown>} Schema
*/


export const SCHEMA_KEYWORD = '$schema';

export const ID_KEYWORD = '$id';
Expand Down Expand Up @@ -88,7 +90,7 @@ export const subschemas = function* (s) {
};


export const REF_FRAG_SEP = '#';
const FRAG_SEP = '#';

/**
* @typedef {object} RefSplit
Expand All @@ -105,9 +107,9 @@ export const REF_FRAG_SEP = '#';
* @returns {RefSplit}
*/
export const refSplit = (r) => {
const split = r.split(REF_FRAG_SEP);
const split = r.split(FRAG_SEP);
if (split.length > 2) {
throw new RangeError(`Unexpected amount of "${REF_FRAG_SEP}" in ${r}`);
throw new RangeError(`Unexpected amount of "${FRAG_SEP}" in ${r}`);
}
return {
path: split[0] || null,
Expand All @@ -122,10 +124,10 @@ export const refSplit = (r) => {
* @returns {string}
*/
export const refJoin = ({path, frag}) => {
if (path && frag) return `${path}${REF_FRAG_SEP}${frag}`;
if (path && frag) return `${path}${FRAG_SEP}${frag}`;
if (path) return path;
if (frag) return `${REF_FRAG_SEP}${frag}`;
return REF_FRAG_SEP; // the entire schema
if (frag) return `${FRAG_SEP}${frag}`;
return FRAG_SEP; // the entire schema
};

/**
Expand All @@ -139,8 +141,26 @@ const URI_OR_ABS_WIN_RE = Object.freeze(/^[a-z][a-z0-9+.-]*:/i);
* @returns {boolean}
*/
export const refIsAbsolute = (r) => {
if (r.startsWith(REF_FRAG_SEP)) return false;
if (refIsLocal(r)) return false;
return r.startsWith('/') // Unix-like absolute path
|| r.startsWith('\\') // UNC-like absolute path
|| URI_OR_ABS_WIN_RE.test(r);
};

/**
* Local reference: a JSON Pointer prefixed with {@link FRAG_SEP}.
* @typedef {`${typeof FRAG_SEP}${string}`} LocalRef
*/

/**
* Whether a ref is local.
* @param {string} r ref
* @return {r is LocalRef}
*/
export const refIsLocal = (r) => r.startsWith(FRAG_SEP);

/**
* @param {string} s
* @return {LocalRef}
*/
export const makeLocalRef = (s) => `${FRAG_SEP}${s}`;
32 changes: 25 additions & 7 deletions tools/src/main/js/bundler/src/sanity.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import {objectGetOwnPath, PATH_NOT_FOUND} from "./helpers/common.js";
import {REF_FRAG_SEP, REF_KEYWORDS, subschemas} from './helpers/json-schema-spec.js';
import {isJsonPointer, jsonPointer2stack} from './helpers/json-pointer-spec.js'
import {
isSchemaObject,
makeLocalRef,
REF_KEYWORDS,
refIsLocal,
refSplit,
subschemas
} from './helpers/json-schema-spec.js';
import {isJsonPointer, jsonPointer2stack, ROOT_POINTER} from './helpers/json-pointer-spec.js'

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

Expand Down Expand Up @@ -33,16 +40,20 @@ const validateRefs = (schema) => {
/** @type {Set<string>} */
const localRefs = new Set();

/** @type {WeakSet<Schema>} */
const _seen = new WeakSet();
/**
* @param {Readonly<Schema>} schema
* @returns {void}
*/
const gatherRefs = (schema) => {
if (_seen.has(schema)) return;
_seen.add(schema);
for (const k of REF_KEYWORDS) {
const v = schema[k];
if (typeof v !== 'string') continue;
if (v.startsWith(REF_FRAG_SEP)) {
localRefs.add(v.slice(REF_FRAG_SEP.length));
if (refIsLocal(v)) {
localRefs.add(refSplit(v).frag ?? ROOT_POINTER);
} else {
warnings.add(`Unchecked external ref: ${v}`);
}
Expand All @@ -54,11 +65,18 @@ const validateRefs = (schema) => {

for (const ref of localRefs) {
if (isJsonPointer(ref)) {
if (objectGetOwnPath(schema, jsonPointer2stack(ref)) === PATH_NOT_FOUND) {
errors.add(`Missing local ref: ${REF_FRAG_SEP}${ref}`);
const rTarget = objectGetOwnPath(schema, jsonPointer2stack(ref));
if (rTarget === PATH_NOT_FOUND) {
errors.add(`Missing local ref target: ${makeLocalRef(ref)}`);
}
if (isSchemaObject(rTarget)) {
gatherRefs(rTarget);
} else if (typeof rTarget !== 'boolean') {
// ref must resolve to a schema or literal boolean
errors.add(`Illegal (non-schema) local ref target: ${makeLocalRef(ref)}`);
}
} else {
warnings.add(`Unchecked local ref: ${REF_FRAG_SEP}${ref}`);
warnings.add(`Unchecked local ref: ${makeLocalRef(ref)}`);
}
}

Expand Down
12 changes: 0 additions & 12 deletions tools/src/main/js/bundler/test/fixtures/main.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,6 @@
"description": "The last alternative of the module's choice.",
"$ref": "modules/used-array-items.schema.json#/properties/choice/oneOf/1"
},
"statusValues": {
"$comment": "Ref to a NON-schema value (the module's enum array). Semantically dubious, but a legal pointer -> the tree-shaker must record it as reached (no crash) and keep the enum intact.",
"title": "Status values",
"description": "Pointer to the module's status enum.",
"$ref": "modules/used-array-items.schema.json#/properties/status/enum"
},
"midLevel": {
"$comment": "Pointer to ONE ITEM of a NON-schema array (enum/1) of a schema that is itself not reached. The item must stay at index 1; its siblings may become void placeholders, like in schema arrays.",
"title": "Mid level",
"description": "The middle level literal.",
"$ref": "modules/used-array-items.schema.json#/properties/level/enum/1"
},
"anyValue": {
"$comment": "Ref to an EMPTY schema `{}`. Reached -> must survive the sweep even though it has no keys at all.",
"title": "Any value",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,27 +80,6 @@
}
],
"examples": [null, 1, 2, 3]
},
"status": {
"$comment": "A NON-schema value of this schema (the enum array) is referenced from main via #/properties/status/enum. The enum must be kept as a whole; 'status' itself is not reached -> hollowed; its 'type' is dropped.",
"title": "Status",
"description": "A status value.",
"type": "string",
"enum": [
"on",
"off"
]
},
"level": {
"$comment": "A single ITEM of a data array (enum/1) is referenced from main. Legal pointer, semantically dubious. 'level' itself is not reached -> hollowed, so the enum has no validation meaning anymore; enum/0 and enum/2 may become void placeholders so that the index of enum/1 stays stable. 'type' is dropped.",
"title": "Level",
"description": "A level value.",
"type": "string",
"enum": [
"low",
"mid",
"high"
]
}
},
"$defs": {
Expand Down
Loading
Loading