build(visual): bundle the visual contract and validation modules into the standalone artifact
The standalone build's source list is explicit, so the two modules the previous commit added were called by validator.js inside XZBT.html without being defined there. Both are now bundled ahead of validator.js and resolution.js, which are their only consumers. visual-validation.js used a local isPlainObject that would have collided with validator.js's own once the bundler strips imports and concatenates into one scope; it uses the shared isRecord of types.js instead. The rebuilt artifact parses and carries validateVisualSubsystem and matchVisualTarget. npm test remains at 131 passing. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_0162Jb1J36judZNT8fHabGVt
This commit is contained in:
@@ -28,7 +28,7 @@ import {
|
||||
VISUAL_LIMITS,
|
||||
VISUAL_SYSTEM_TYPES
|
||||
} from './visual-contract.js';
|
||||
import { DURATION_PATTERN } from './types.js';
|
||||
import { DURATION_PATTERN, isRecord } from './types.js';
|
||||
|
||||
const AUTHORING = VISUAL_LIMITS.authoring;
|
||||
|
||||
@@ -72,10 +72,6 @@ const SYSTEM_AUTOMATABLE = Object.freeze({
|
||||
|
||||
const EFFECT_INDEX = /^effects\[(\d+)\]\.([A-Za-z][A-Za-z0-9]*)$/;
|
||||
|
||||
function isPlainObject(value) {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isColor(value) {
|
||||
return typeof value === 'string' && value.length > 0;
|
||||
}
|
||||
@@ -85,7 +81,7 @@ export function validateVisualSubsystem(document, errors, helpers) {
|
||||
const fail = (code, path, message) => pushError(errors, code, path, message);
|
||||
const visuals = document.visuals;
|
||||
if (visuals === undefined) return;
|
||||
if (!isPlainObject(visuals)) return fail('ERR_SCHEMA_VALIDATION', '$.visuals', 'visuals must be an object.');
|
||||
if (!isRecord(visuals)) return fail('ERR_SCHEMA_VALIDATION', '$.visuals', 'visuals must be an object.');
|
||||
|
||||
for (const field of Object.keys(visuals)) {
|
||||
if (!VISUALS_FIELDS.has(field)) fail('ERR_UNKNOWN_FIELD', `$.visuals.${field}`, `Unrecognized visuals field '${field}'.`);
|
||||
@@ -104,8 +100,8 @@ export function validateVisualSubsystem(document, errors, helpers) {
|
||||
const count = (result) => { tracks += result.tracks; points += result.points; };
|
||||
|
||||
count(validateAutomationArray(document, visuals.automation, '$.visuals.automation', { scope: 'exhibit', context }, errors, helpers));
|
||||
for (const [id, system] of Object.entries(isPlainObject(visuals.systems) ? visuals.systems : {})) {
|
||||
if (!isPlainObject(system)) continue;
|
||||
for (const [id, system] of Object.entries(isRecord(visuals.systems) ? visuals.systems : {})) {
|
||||
if (!isRecord(system)) continue;
|
||||
count(validateAutomationArray(document, system.automation, `$.visuals.systems.${id}.automation`, { scope: 'system', systemId: id, system, context }, errors, helpers));
|
||||
}
|
||||
|
||||
@@ -123,7 +119,7 @@ function validateScene(document, scene, errors, { validateValueSpec, pushError }
|
||||
fail('ERR_SCHEMA_VALIDATION', '$.visuals.scene', 'visuals.scene is required when visuals is present.');
|
||||
return {};
|
||||
}
|
||||
if (!isPlainObject(scene)) {
|
||||
if (!isRecord(scene)) {
|
||||
fail('ERR_SCHEMA_VALIDATION', '$.visuals.scene', 'scene must be an object.');
|
||||
return {};
|
||||
}
|
||||
@@ -151,7 +147,7 @@ function validateScene(document, scene, errors, { validateValueSpec, pushError }
|
||||
|
||||
if (scene.depthFog !== undefined) {
|
||||
const fog = scene.depthFog;
|
||||
if (!isPlainObject(fog)) fail('ERR_SCHEMA_VALIDATION', '$.visuals.scene.depthFog', 'depthFog must be an object.');
|
||||
if (!isRecord(fog)) fail('ERR_SCHEMA_VALIDATION', '$.visuals.scene.depthFog', 'depthFog must be an object.');
|
||||
else {
|
||||
for (const field of Object.keys(fog)) if (!DEPTH_FOG_FIELDS.has(field)) fail('ERR_UNKNOWN_FIELD', `$.visuals.scene.depthFog.${field}`, `Unrecognized depthFog field '${field}'.`);
|
||||
if (!isColor(fog.color)) fail('ERR_SCHEMA_VALIDATION', '$.visuals.scene.depthFog.color', 'depthFog.color is required.');
|
||||
@@ -166,7 +162,7 @@ function validateScene(document, scene, errors, { validateValueSpec, pushError }
|
||||
function validateLayers(document, layers, errors, { validateValueSpec, pushError }) {
|
||||
const fail = (code, path, message) => pushError(errors, code, path, message);
|
||||
if (layers === undefined) return null;
|
||||
if (!isPlainObject(layers)) {
|
||||
if (!isRecord(layers)) {
|
||||
fail('ERR_SCHEMA_VALIDATION', '$.visuals.layers', 'layers must be an object.');
|
||||
return null;
|
||||
}
|
||||
@@ -181,7 +177,7 @@ function validateLayers(document, layers, errors, { validateValueSpec, pushError
|
||||
for (const [id, layer] of Object.entries(layers)) {
|
||||
const path = `$.visuals.layers.${id}`;
|
||||
if (!ID_PATTERN.test(id)) fail('ERR_INVALID_ID', path, `Layer ID '${id}' is invalid.`);
|
||||
if (!isPlainObject(layer)) { fail('ERR_SCHEMA_VALIDATION', path, 'Layer must be an object.'); continue; }
|
||||
if (!isRecord(layer)) { fail('ERR_SCHEMA_VALIDATION', path, 'Layer must be an object.'); continue; }
|
||||
for (const field of Object.keys(layer)) if (!LAYER_FIELDS.has(field)) fail('ERR_UNKNOWN_FIELD', `${path}.${field}`, `Unrecognized layer field '${field}'.`);
|
||||
if (layer.opacity !== undefined) validateValueSpec(document, layer.opacity, `${path}.opacity`, errors);
|
||||
if (layer.visible !== undefined) validateValueSpec(document, layer.visible, `${path}.visible`, errors);
|
||||
@@ -194,7 +190,7 @@ function validateLayers(document, layers, errors, { validateValueSpec, pushError
|
||||
function validateCamera(document, camera, errors, { validateValueSpec, pushError }) {
|
||||
const fail = (code, path, message) => pushError(errors, code, path, message);
|
||||
if (camera === undefined) return;
|
||||
if (!isPlainObject(camera)) return fail('ERR_SCHEMA_VALIDATION', '$.visuals.camera', 'camera must be an object.');
|
||||
if (!isRecord(camera)) return fail('ERR_SCHEMA_VALIDATION', '$.visuals.camera', 'camera must be an object.');
|
||||
for (const field of Object.keys(camera)) {
|
||||
if (!CAMERA_ALLOWED.has(field)) fail('ERR_UNKNOWN_FIELD', `$.visuals.camera.${field}`, `Unrecognized camera field '${field}'.`);
|
||||
}
|
||||
@@ -223,7 +219,7 @@ function validateEffects(document, effects, errors, { validateValueSpec, pushErr
|
||||
if (effects.length > AUTHORING.postEffectEntries) fail('ERR_VISUAL_LIMIT_EXCEEDED', '$.visuals.effects', `At most ${AUTHORING.postEffectEntries} post-effect entries.`);
|
||||
effects.forEach((entry, index) => {
|
||||
const path = `$.visuals.effects[${index}]`;
|
||||
if (!isPlainObject(entry)) return fail('ERR_SCHEMA_VALIDATION', path, 'Effect entry must be an object.');
|
||||
if (!isRecord(entry)) return fail('ERR_SCHEMA_VALIDATION', path, 'Effect entry must be an object.');
|
||||
const definition = POST_EFFECTS[entry.type];
|
||||
if (!definition) return fail('ERR_INVALID_EFFECT_TYPE', `${path}.type`, `Unsupported post-effect type '${entry.type}'.`);
|
||||
for (const field of Object.keys(entry)) {
|
||||
@@ -252,7 +248,7 @@ function validateEffects(document, effects, errors, { validateValueSpec, pushErr
|
||||
function validateFields(document, fields, errors, { validateValueSpec, pushError }) {
|
||||
const fail = (code, path, message) => pushError(errors, code, path, message);
|
||||
if (fields === undefined) return new Set();
|
||||
if (!isPlainObject(fields)) {
|
||||
if (!isRecord(fields)) {
|
||||
fail('ERR_SCHEMA_VALIDATION', '$.visuals.fields', 'fields must be an object.');
|
||||
return new Set();
|
||||
}
|
||||
@@ -261,7 +257,7 @@ function validateFields(document, fields, errors, { validateValueSpec, pushError
|
||||
for (const [id, field] of Object.entries(fields)) {
|
||||
const path = `$.visuals.fields.${id}`;
|
||||
if (!ID_PATTERN.test(id)) fail('ERR_INVALID_ID', path, `Field ID '${id}' is invalid.`);
|
||||
if (!isPlainObject(field)) { fail('ERR_SCHEMA_VALIDATION', path, 'Field must be an object.'); continue; }
|
||||
if (!isRecord(field)) { fail('ERR_SCHEMA_VALIDATION', path, 'Field must be an object.'); continue; }
|
||||
if (!VISUAL_FIELD_TYPES.includes(field.type)) { fail('ERR_INVALID_FIELD_TYPE', `${path}.type`, `Unsupported field type '${field.type}'.`); continue; }
|
||||
const allowed = new Set([...FIELD_COMMON, ...FIELD_TYPE_FIELDS[field.type]]);
|
||||
for (const key of Object.keys(field)) if (!allowed.has(key)) fail('ERR_UNKNOWN_FIELD', `${path}.${key}`, `Field type '${field.type}' declares no '${key}'.`);
|
||||
@@ -283,7 +279,7 @@ function validateFields(document, fields, errors, { validateValueSpec, pushError
|
||||
function validateSystems(document, systems, layers, fields, errors, { validateValueSpec, pushError }) {
|
||||
const fail = (code, path, message) => pushError(errors, code, path, message);
|
||||
if (systems === undefined) return {};
|
||||
if (!isPlainObject(systems)) {
|
||||
if (!isRecord(systems)) {
|
||||
fail('ERR_SCHEMA_VALIDATION', '$.visuals.systems', 'systems must be an object.');
|
||||
return {};
|
||||
}
|
||||
@@ -292,7 +288,7 @@ function validateSystems(document, systems, layers, fields, errors, { validateVa
|
||||
for (const [id, system] of Object.entries(systems)) {
|
||||
const path = `$.visuals.systems.${id}`;
|
||||
if (!ID_PATTERN.test(id)) fail('ERR_INVALID_ID', path, `System ID '${id}' is invalid.`);
|
||||
if (!isPlainObject(system)) { fail('ERR_SCHEMA_VALIDATION', path, 'System must be an object.'); continue; }
|
||||
if (!isRecord(system)) { fail('ERR_SCHEMA_VALIDATION', path, 'System must be an object.'); continue; }
|
||||
if (!VISUAL_SYSTEM_TYPES.includes(system.type)) fail('ERR_INVALID_SYSTEM_TYPE', `${path}.type`, `Unsupported visual system type '${system.type}'.`);
|
||||
|
||||
// 17.7 (V17): the presence of the layer map decides whether `layer` is required.
|
||||
@@ -335,7 +331,7 @@ function validateSpawn(document, system, lifecycle, path, errors, { validateValu
|
||||
const spawn = system.spawn;
|
||||
if (spawn === undefined) return;
|
||||
if (lifecycle !== 'spawned') return fail('ERR_UNKNOWN_FIELD', `${path}.spawn`, 'A persistent system has no spawn container.');
|
||||
if (!isPlainObject(spawn)) return fail('ERR_SCHEMA_VALIDATION', `${path}.spawn`, 'spawn must be an object.');
|
||||
if (!isRecord(spawn)) return fail('ERR_SCHEMA_VALIDATION', `${path}.spawn`, 'spawn must be an object.');
|
||||
for (const field of Object.keys(spawn)) {
|
||||
if (!SPAWN_FIELDS.includes(field)) fail('ERR_UNKNOWN_FIELD', `${path}.spawn.${field}`, `Unrecognized spawn field '${field}'.`);
|
||||
}
|
||||
@@ -350,7 +346,7 @@ function validateSpawn(document, system, lifecycle, path, errors, { validateValu
|
||||
// the instance's resources belong to the performance root.
|
||||
else if (spawn.cancelWithScenario === false && spawn.ownership !== 'persistent') fail('ERR_UNSUPPORTED_TARGET', `${path}.spawn.cancelWithScenario`, "cancelWithScenario: false requires ownership: 'persistent'.");
|
||||
}
|
||||
if (spawn.inputs !== undefined && !isPlainObject(spawn.inputs)) fail('ERR_SCHEMA_VALIDATION', `${path}.spawn.inputs`, 'spawn.inputs must be an object.');
|
||||
if (spawn.inputs !== undefined && !isRecord(spawn.inputs)) fail('ERR_SCHEMA_VALIDATION', `${path}.spawn.inputs`, 'spawn.inputs must be an object.');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -369,14 +365,14 @@ function validateAutomationArray(document, tracks, path, scope, errors, helpers)
|
||||
const written = new Set();
|
||||
tracks.forEach((track, index) => {
|
||||
const location = `${path}[${index}]`;
|
||||
if (!isPlainObject(track)) return fail('ERR_SCHEMA_VALIDATION', location, 'Automation track must be an object.');
|
||||
if (!isRecord(track)) return fail('ERR_SCHEMA_VALIDATION', location, 'Automation track must be an object.');
|
||||
for (const field of Object.keys(track)) if (!TRACK_FIELDS.has(field)) fail('ERR_UNKNOWN_FIELD', `${location}.${field}`, `Unrecognized automation field '${field}'.`);
|
||||
if (track.mode !== undefined && !AUTOMATION_MODES.includes(track.mode)) fail('ERR_SCHEMA_VALIDATION', `${location}.mode`, `Unsupported automation mode '${track.mode}'.`);
|
||||
if (track.interpolation !== undefined && !AUTOMATION_CURVES.includes(track.interpolation)) fail('ERR_SCHEMA_VALIDATION', `${location}.interpolation`, `Unsupported interpolation '${track.interpolation}'.`);
|
||||
|
||||
if (track.loop !== undefined) {
|
||||
const loop = track.loop;
|
||||
if (!isPlainObject(loop)) fail('ERR_SCHEMA_VALIDATION', `${location}.loop`, 'loop must be an object.');
|
||||
if (!isRecord(loop)) fail('ERR_SCHEMA_VALIDATION', `${location}.loop`, 'loop must be an object.');
|
||||
else {
|
||||
for (const field of Object.keys(loop)) if (field !== 'mode' && field !== 'count') fail('ERR_UNKNOWN_FIELD', `${location}.loop.${field}`, `Unrecognized loop field '${field}'.`);
|
||||
if (!LOOP_MODES.includes(loop.mode)) fail('ERR_SCHEMA_VALIDATION', `${location}.loop.mode`, `Unsupported loop mode '${loop.mode}'.`);
|
||||
@@ -395,7 +391,7 @@ function validateAutomationArray(document, tracks, path, scope, errors, helpers)
|
||||
let previous = -Infinity;
|
||||
track.points.forEach((point, pointIndex) => {
|
||||
const pointPath = `${location}.points[${pointIndex}]`;
|
||||
if (!isPlainObject(point)) return fail('ERR_SCHEMA_VALIDATION', pointPath, 'Automation point must be an object.');
|
||||
if (!isRecord(point)) return fail('ERR_SCHEMA_VALIDATION', pointPath, 'Automation point must be an object.');
|
||||
for (const field of Object.keys(point)) if (field !== 'at' && field !== 'value') fail('ERR_UNKNOWN_FIELD', `${pointPath}.${field}`, `Unrecognized point field '${field}'.`);
|
||||
// 19.1: `at` is a duration literal only, never a procedural TimeSpec,
|
||||
// so the strictly-increasing rule stays decidable at import.
|
||||
@@ -485,7 +481,7 @@ function resolveGraphicTarget(target, system) {
|
||||
let container = system?.content;
|
||||
let index = 0;
|
||||
while (index < parts.length) {
|
||||
if (!isPlainObject(container) || !Object.hasOwn(container, parts[index])) {
|
||||
if (!isRecord(container) || !Object.hasOwn(container, parts[index])) {
|
||||
return { code: 'ERR_INVALID_REFERENCE', message: `'${target}' does not name an object in this system's content.` };
|
||||
}
|
||||
const object = container[parts[index]];
|
||||
@@ -494,7 +490,7 @@ function resolveGraphicTarget(target, system) {
|
||||
if (remainder.length === 0) return { code: 'ERR_UNSUPPORTED_TARGET', message: `'${target}' names an object, not a numeric property of one.` };
|
||||
if (GRAPHIC_NUMERIC_PROPERTIES.has(remainder) || POINT_PROPERTY.test(remainder)) return { key: target };
|
||||
// Not a property tail, so the next segment must be a container key.
|
||||
if (isPlainObject(object) && (object.type === 'group' || object.type === 'component') && isPlainObject(object.children)) {
|
||||
if (isRecord(object) && (object.type === 'group' || object.type === 'component') && isRecord(object.children)) {
|
||||
container = object.children;
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user