// Generates the cross-language parity fixtures consumed by every port's test // suite. Re-run via `npm run fixtures:parity` whenever the JS implementation // legitimately changes; commit the diff and bring the other ports back in // sync. // // The generator deliberately uses the JS implementation as the reference. // The other ports are then expected to match these byte-for-byte. import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { Avatar, Color, OptionsDescriptor, Style, } from '../../../src/js/core/lib/index.js'; import { Prng } from '../../../src/js/core/lib/Prng.js'; import { Fnv1a } from '../../../src/js/core/lib/Prng/Fnv1a.js'; import { Mulberry32 } from '../../../src/js/core/lib/Prng/Mulberry32.js'; import { Number } from '../../../src/js/core/lib/Utils/Number.js'; import { Initials } from '../../../src/js/core/lib/Utils/Initials.js'; const definitionsDir = join( import.meta.dirname, '..', '..', '..', '..', 'styles', 'dist', ); const STYLE_NAMES = ['initials', 'thumbs', 'glass', 'notionists', 'shape-grid']; // JSON.stringify leaves U+2028/U+2029 (line/paragraph separators) and BMP // private-use code points literal; editors flag those as unusual line // terminators or invisible glyphs. Rewrite them as \uXXXX so the committed // fixtures hold no such characters. Built from char codes so this source file // stays clean too. function escapeInvisible(json) { let out = ''; for (const ch of json) { const cp = ch.codePointAt(0); if (cp === 0x2028 || cp === 0x2029 || (cp >= 0xe000 && cp <= 0xf8ff)) { out += String.fromCharCode(0x5c) + 'u' + cp.toString(16).padStart(4, '0'); } else { out += ch; } } return out; } function writeJson(relPath, data) { const filePath = join(import.meta.dirname, relPath); mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, escapeInvisible(JSON.stringify(data, null, 2)) + '\n'); console.log(' wrote', relPath); } // --------------------------------------------------------------------------- // Vendor style definitions // --------------------------------------------------------------------------- console.log('Copying style definitions…'); const styles = {}; for (const name of STYLE_NAMES) { const raw = JSON.parse( readFileSync(join(definitionsDir, `${name}.min.json`), 'utf8'), ); styles[name] = raw; writeJson(join('styles', `${name}.json`), raw); } // --------------------------------------------------------------------------- // Fnv1a fixtures // --------------------------------------------------------------------------- console.log('Generating fnv1a.json…'); const fnv1aInputs = [ '', 'a', 'b', 'c', 'hello', 'test', 'foobar', '123', 'dicebear', 'test:flip', 'test:scale', 'some-long-seed:optionName', 'é', '日本語', '🎲', 'a'.repeat(256), ]; writeJson( 'fnv1a.json', fnv1aInputs.map((input) => ({ input, hash: Fnv1a.hash(input), hex: Fnv1a.hex(input), })), ); // --------------------------------------------------------------------------- // Mulberry32 fixtures // --------------------------------------------------------------------------- console.log('Generating mulberry32.json…'); const mulberrySeeds = [0, 1, 2, 42, 100, 0x811c9dc5, 0xffffffff]; writeJson( 'mulberry32.json', mulberrySeeds.map((seed) => { const m = new Mulberry32(seed); const sequence = []; for (let i = 0; i < 5; i++) { const float = m.nextFloat(); sequence.push({ float, state: m.state() }); } return { seed, sequence }; }), ); // --------------------------------------------------------------------------- // Prng fixtures (key-based methods) // --------------------------------------------------------------------------- console.log('Generating prng.json…'); const prngFixtures = { getValue: [], pick: [], weightedPick: [], bool: [], float: [], integer: [], shuffle: [], }; const getValueCases = [ { seed: 'test', key: 'flip' }, { seed: 'test', key: 'scale' }, { seed: 'test', key: 'rotate' }, { seed: 'test', key: 'borderRadius' }, { seed: 'test', key: 'fontWeight' }, { seed: 'hello', key: 'flip' }, { seed: 'hello', key: 'eyesVariant' }, { seed: 'dicebear', key: 'colorFill' }, { seed: '日本語', key: 'unicode-key' }, { seed: '', key: 'empty-seed' }, ]; for (const c of getValueCases) { prngFixtures.getValue.push({ ...c, result: new Prng(c.seed).getValue(c.key), }); } // Supplementary-plane test characters, built via code point / code unit so the // source holds no literal control characters. U+1F600's UTF-16 lead surrogate // (D83D) sorts BEFORE U+E000 by code unit but AFTER it by code point — so these // pin that every port sorts by UTF-16 code units, not scalar values. const EMOJI = String.fromCodePoint(0x1f600); const PUA = String.fromCharCode(0xe000); const pickCases = [ { seed: 'test', key: 'a', items: ['a', 'b', 'c', 'd', 'e'] }, { seed: 'test', key: 'b', items: ['a', 'b', 'c', 'd', 'e'] }, { seed: 'test', key: 'c', items: ['a', 'b', 'c', 'd', 'e'] }, { seed: 'hello', key: 'eyes', items: ['blue', 'green', 'brown', 'hazel'] }, { seed: 'dicebear', key: 'variant', items: ['variant1', 'variant2', 'variant3'] }, { seed: 'test', key: 'single', items: ['only'] }, // pre-shuffled order to verify that sorting normalizes input { seed: 'test', key: 'a', items: ['e', 'd', 'c', 'b', 'a'] }, // duplicates must be collapsed — same result as the unique set { seed: 'test', key: 'a', items: ['a', 'a', 'b', 'b', 'c', 'c', 'd', 'd', 'e', 'e'] }, { seed: 'test', key: 'single', items: ['only', 'only', 'only'] }, // A two-element astral/BMP pair distinguishes the sort whichever index hits. { seed: 'test', key: 'a', items: [EMOJI, PUA] }, { seed: 'hello', key: 'k', items: ['z', PUA, EMOJI, 'a'] }, ]; for (const c of pickCases) { prngFixtures.pick.push({ ...c, result: new Prng(c.seed).pick(c.key, c.items), }); } const weightedPickCases = [ { seed: 'test', key: 'k', weights: { a: 1, b: 4, c: 2 } }, { seed: 'test', key: 'l', weights: { a: 1, b: 4, c: 2 } }, { seed: 'hello', key: 'k', weights: { a: 1, b: 4, c: 2 } }, { seed: 'test', key: 'k', weights: { heavy: 100, light: 1 } }, { seed: 'test', key: 'k', weights: { rare: 0, common: 1 } }, // insertion-order independence: same expected result as the first entry above { seed: 'test', key: 'k', weights: { c: 2, a: 1, b: 4 } }, { seed: 'test', key: 'k', weights: { only: 1 } }, // fractional weights in non-sorted insertion order — locks in that every port // sums in the same order (sorted), since float addition is non-associative { seed: 'test', key: 'k', weights: { c: 0.1, a: 0.2, b: 0.3 } }, // Astral-plane keys: UTF-16 vs code-point sort order (see EMOJI/PUA above). { seed: 'test', key: 'k', weights: { [EMOJI]: 1, [PUA]: 2, a: 3 } }, ]; for (const c of weightedPickCases) { prngFixtures.weightedPick.push({ ...c, result: new Prng(c.seed).weightedPick(c.key, c.weights), }); } const boolCases = [ { seed: 'test', key: 'k', likelihood: 0 }, { seed: 'test', key: 'k', likelihood: 25 }, { seed: 'test', key: 'k', likelihood: 50 }, { seed: 'test', key: 'k', likelihood: 75 }, { seed: 'test', key: 'k', likelihood: 100 }, { seed: 'hello', key: 'flip', likelihood: 50 }, { seed: 'dicebear', key: 'flip', likelihood: 50 }, ]; for (const c of boolCases) { prngFixtures.bool.push({ ...c, result: new Prng(c.seed).bool(c.key, c.likelihood), }); } const floatCases = [ { seed: 'test', key: 'k', range: { min: 0, max: 1 } }, { seed: 'test', key: 'k', range: { min: -10, max: 10 } }, { seed: 'test', key: 'k', range: { min: 10, max: 20 } }, { seed: 'test', key: 'k', range: { min: 20, max: 10 } }, // reversed { seed: 'test', key: 'k', range: { min: 0, max: 100, step: 5 } }, // stepped { seed: 'test', key: 'k', range: { min: 3, max: 23, step: 5 } }, // stepped, min not on step grid { seed: 'parity-step', key: 'k', range: { min: -10, max: 10, step: 2.5 } }, // stepped, float step { seed: 'test', key: 'k', range: { min: 10, max: 20, step: 0 } }, // step 0 → continuous { seed: 'hello', key: 'scale', range: { min: 0.5, max: 1.5 } }, { seed: 'test', key: 'k', range: { min: 42, max: 42 } }, // fixed // Negative value landing exactly on a .5 boundary after *10000 // (value = -0.40625 → -4062.5). Pins the rounding mode: the reference rounds // half toward +Infinity (-0.4062), not away from zero (-0.4063). { seed: '549449', key: 'colorAngle', range: { min: -1, max: 1 } }, ]; for (const c of floatCases) { prngFixtures.float.push({ ...c, result: new Prng(c.seed).float(c.key, c.range), }); } const integerCases = [ { seed: 'test', key: 'k', range: { min: 1, max: 6 } }, { seed: 'test', key: 'k', range: { min: 0, max: 100 } }, { seed: 'test', key: 'k', range: { min: -50, max: 50 } }, { seed: 'test', key: 'k', range: { min: 10, max: 1 } }, // reversed { seed: 'test', key: 'k', range: { min: 3, max: 10, step: 7 } }, // step ignored { seed: 'test', key: 'k', range: { min: 5, max: 5 } }, // min == max { seed: 'hello', key: 'fontWeight', range: { min: 100, max: 900 } }, ]; for (const c of integerCases) { prngFixtures.integer.push({ ...c, result: new Prng(c.seed).integer(c.key, c.range), }); } const shuffleCases = [ { seed: 'test', key: 'k', items: [] }, { seed: 'test', key: 'k', items: ['only'] }, { seed: 'test', key: 'k', items: ['a', 'b', 'c'] }, { seed: 'test', key: 'k', items: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] }, { seed: 'hello', key: 'k', items: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] }, // pre-shuffled order to verify that sorting normalizes input { seed: 'test', key: 'k', items: ['h', 'g', 'f', 'e', 'd', 'c', 'b', 'a'] }, // duplicates collapse — output length matches unique-set length { seed: 'test', key: 'k', items: ['a', 'a', 'b', 'b', 'c', 'c'] }, // Astral-plane items: UTF-16 vs code-point sort order (see EMOJI/PUA above). { seed: 'test', key: 'k', items: ['z', PUA, EMOJI, 'a'] }, ]; for (const c of shuffleCases) { prngFixtures.shuffle.push({ ...c, result: new Prng(c.seed).shuffle(c.key, c.items), }); } writeJson('prng.json', prngFixtures); // --------------------------------------------------------------------------- // Number formatting fixtures // --------------------------------------------------------------------------- // // The renderer stringifies every numeric SVG attribute via `Number.format`, // which rounds to at most 5 decimal places. PHP's native float cast and // Python's `repr` would diverge from JS for small/large magnitudes, so each // port reimplements it (`DiceBear\Utils\Number::format`, // `dicebear.utils.Number.format`) and must reproduce it byte-for-byte. These pin // that contract — covering integers, plain decimals, values that round at the // 5th decimal, and tiny values that round down to 0. console.log('Generating numbers.json…'); const numberInputs = [ 0, 1, -1, 100, -50, 4096, 1000000, 0.5, 1.5, -1.5, 2.5, 0.1, 0.3, 12.34, 123.456, -148.722, 9999.9999, 0.00001, 0.00005, 0.0001, 0.000005, 0.0000005, 0.0000123456, 4.567872, 0.123456, 1.999995, 1.999994, 0.123455, 0.123445, 5e-7, 1e-7, 1.5e-6, 0.0009999999999999998, 4.9999999999999996e-6, -4.567872, -0.000005, -0.0001, ]; writeJson( 'numbers.json', numberInputs.map((input) => ({ input, output: Number.format(input) })), ); // --------------------------------------------------------------------------- // Initials fixtures // --------------------------------------------------------------------------- // // `Initials.fromSeed` derives display initials for the `initials` style. The // `@`-stripping, quote-stripping, and `\p{L}` word matching are easy to get // subtly wrong per language (e.g. a regex that strips raw bytes instead of code // points would corrupt multibyte letters like ô/ü). These cases pin the // contract across ASCII, accented, quote-prefixed, email, CJK, and emoji seeds. console.log('Generating initials.json…'); const initialsSeeds = [ 'AB CD', 'café', 'über', 'côté', 'grün rot', "l'eau", 'd´or', 'ʼtest', 'user@example.com', 'a', '', '日本語', '🎲 spiel', 'Œuvre', 'rock-paper', // The whole @ suffix is stripped, including line terminators (dotall): these // seeds verify a CR / U+2028 / U+2029 right after `@` is removed, not kept. 'a@b' + String.fromCharCode(0x0d) + 'cd', 'a@b' + String.fromCharCode(0x2028) + 'cd', 'a@b' + String.fromCharCode(0x2029) + 'cd', // Full Unicode case mapping (not simple 1:1): ß→SS, the fi ligature→FI, and // Greek iota-subscript forms expand to two letters. Languages whose default // uppercasing is simple-mapping only (Go strings.ToUpper, Dart VM // toUpperCase) need a full-mapping implementation to pass these. 'ß test', 'fish bar', 'ᾀeolic text', ]; writeJson( 'initials.json', initialsSeeds.map((seed) => ({ seed, result: Initials.fromSeed(seed) })), ); // --------------------------------------------------------------------------- // Color fixtures // --------------------------------------------------------------------------- // // The color helpers feed both the resolver (contrast sorting picks colors, so // any drift changes rendered avatars) and public API consumers. `luminance` // involves float math (sRGB linearization with `** 2.4`), where rounding could // silently diverge between languages; the fixtures pin exact doubles. console.log('Generating colors.json…'); // Covers 3-, 4-, 6- and 8-digit forms, with/without `#`, and mixed case. const hexInputs = [ '000', '#000', 'abc', '#AbC', 'f0f0', '#F0f0', 'ff0000', '#FF0000', 'ff000080', '#ff000080', '#1a2b3c', ]; const luminanceInputs = [ '#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff', '#808080', '#0a5b83', '#69d2e7', '#f88c49', '#abc', '#1a2b3c', // Around the sRGB linearization threshold (s <= 0.04045 → channel <= ~10.3): // 0x0a takes the linear branch, 0x0b the power branch. '#0a0a0a', '#0b0b0b', ]; const sortByContrastCases = [ { candidates: ['#000000', '#ffffff', '#ff0000', '#0000ff'], refColor: '#ffffff', }, { candidates: ['#69d2e7', '#0a5b83', '#f88c49', '#e0e4cc'], refColor: '#000000', }, // Equal ratios (same color in different notations) must keep input order — // pins that every port sorts stably. { candidates: ['#ff0000', 'ff0000', '#FF0000', '#f00'], refColor: '#00ff00' }, { candidates: [], refColor: '#000000' }, { candidates: ['#123456'], refColor: '#123456' }, ]; const filterNotEqualToCases = [ { candidates: ['#ff0000', '#00ff00', '#0000ff'], excluded: ['ff0000'] }, // The alpha channel is stripped for comparison. { candidates: ['#ff000080', '#00ff00'], excluded: ['#ff0000'] }, // Short forms normalize before comparison. { candidates: ['#abc', '#def'], excluded: ['#aabbcc'] }, // Filtering would empty the list → falls back to the original candidates. { candidates: ['#ff0000', 'ff0000'], excluded: ['#f00'] }, { candidates: ['#ff0000'], excluded: [] }, ]; writeJson('colors.json', { toHex: hexInputs.map((input) => ({ input, result: Color.toHex(input) })), toRgbHex: hexInputs.map((input) => ({ input, result: Color.toRgbHex(input), })), parseHex: hexInputs.map((input) => ({ input, result: Color.parseHex(input) })), luminance: luminanceInputs.map((input) => ({ input, result: Color.luminance(input), })), sortByContrast: sortByContrastCases.map((c) => ({ ...c, result: Color.sortByContrast(c.candidates, c.refColor), })), filterNotEqualTo: filterNotEqualToCases.map((c) => ({ ...c, result: Color.filterNotEqualTo(c.candidates, c.excluded), })), }); // --------------------------------------------------------------------------- // Validation fixtures // --------------------------------------------------------------------------- // // Pins which inputs every port must accept or reject. Only the accept/reject // outcome is shared — error messages are language-specific. The circular // color reference cases additionally pin the reported resolution chain. console.log('Generating validation.json…'); const minimalStyle = { canvas: { width: 100, height: 100, elements: [] } }; const styleValidationCases = [ { id: 'minimal', definition: minimalStyle }, { id: 'with-component-and-color', definition: { canvas: { width: 100, height: 100, elements: [] }, components: { shape: { width: 100, height: 100, variants: { a: { elements: [] }, b: { elements: [] } }, }, }, colors: { fill: { values: ['#000000'] } }, }, }, { id: 'empty', definition: {} }, { id: 'missing-canvas', definition: { components: {} } }, { id: 'canvas-not-object', definition: { canvas: 'nope' } }, { id: 'canvas-missing-size', definition: { canvas: { elements: [] } } }, { id: 'alias-to-unknown-component', definition: { canvas: { width: 100, height: 100, elements: [] }, components: { a: { extends: 'missing' } }, }, }, { id: 'unknown-root-key', definition: { canvas: { width: 100, height: 100, elements: [] }, unexpected: true }, }, ]; const optionsValidationCases = [ { id: 'empty', options: {} }, { id: 'seed-string', options: { seed: 'x' } }, { id: 'seed-number', options: { seed: 123 } }, { id: 'size-min', options: { size: 1 } }, { id: 'size-zero', options: { size: 0 } }, { id: 'size-negative', options: { size: -5 } }, { id: 'size-float', options: { size: 12.5 } }, { id: 'size-huge', options: { size: 2000000 } }, { id: 'flip-valid', options: { flip: 'horizontal' } }, { id: 'flip-invalid', options: { flip: 'diagonal' } }, { id: 'rotate-in-range', options: { rotate: 360 } }, { id: 'rotate-out-of-range', options: { rotate: 361 } }, { id: 'scale-negative', options: { scale: -1 } }, { id: 'title-number', options: { title: 123 } }, { id: 'id-randomization-string', options: { idRandomization: 'yes' } }, { id: 'background-color-string', options: { backgroundColor: 'ff0000' } }, { id: 'background-color-list', options: { backgroundColor: ['ff0000'] } }, { id: 'background-color-invalid-hex', options: { backgroundColor: ['zzz'] } }, { id: 'unknown-option', options: { unknownOption: 1 } }, ]; // A style whose canvas uses color `a`, so resolving it walks the (circular) // `contrastTo` chain. function circularStyle(colors) { return { canvas: { width: 100, height: 100, elements: [ { type: 'element', name: 'rect', attributes: { fill: { type: 'color', name: 'a' } }, }, ], }, colors, }; } const circularColorCases = [ { id: 'self-reference', style: circularStyle({ a: { values: ['#000000'], contrastTo: 'a' } }), }, { id: 'two-color-cycle', style: circularStyle({ a: { values: ['#000000'], contrastTo: 'b' }, b: { values: ['#ffffff'], contrastTo: 'a' }, }), }, { id: 'three-color-cycle', style: circularStyle({ a: { values: ['#000000'], contrastTo: 'b' }, b: { values: ['#ffffff'], contrastTo: 'c' }, c: { values: ['#ff0000'], contrastTo: 'a' }, }), }, ]; const validationFixtures = { styles: styleValidationCases.map((c) => { let valid = true; try { new Style(c.definition); } catch { valid = false; } return { ...c, valid }; }), // Validated against the `minimal` style above. options: optionsValidationCases.map((c) => { let valid = true; try { new Avatar(minimalStyle, c.options); } catch { valid = false; } return { ...c, valid }; }), circularColors: circularColorCases.map((c) => { try { new Avatar(c.style, { seed: 'x' }); } catch (e) { if (e.name !== 'CircularColorReferenceError') { throw new Error( `${c.id}: expected CircularColorReferenceError, got ${e.name}`, { cause: e }, ); } return { ...c, options: { seed: 'x' }, chain: e.chain }; } throw new Error(`${c.id}: expected a circular color reference error`); }), }; writeJson('validation.json', validationFixtures); // --------------------------------------------------------------------------- // Avatar fixtures // --------------------------------------------------------------------------- console.log('Generating avatar fixtures…'); const DATA_URI_CASE_IDS = new Set(['plain-seed', 'title-escaping']); function avatarCases(extra) { return [ { id: 'plain-seed', options: { seed: 'parity-1' }, }, { id: 'different-seed', options: { seed: 'parity-2' }, }, { id: 'size-scale-rotate', options: { seed: 'parity-1', size: 256, scale: 2, rotate: 45 }, }, { id: 'translate-border-flip', options: { seed: 'parity-1', translateX: 10, translateY: -5, borderRadius: 50, flip: 'horizontal', }, }, { id: 'background-solid', options: { seed: 'parity-1', backgroundColor: ['ff0000'], backgroundColorFill: ['solid'], }, }, { id: 'background-linear', options: { seed: 'parity-1', backgroundColor: ['ff0000', '00ff00'], backgroundColorFill: ['linear'], backgroundColorAngle: [45, 45], }, }, { id: 'background-radial', options: { seed: 'parity-1', backgroundColor: ['ff0000', '00ff00'], backgroundColorFill: ['radial'], }, }, // `size` and `title` together pin the resolution order of the resolved // options snapshot (size is recorded before title). { id: 'title-size', options: { seed: 'parity-1', size: 128, title: 'Parity Avatar' }, }, // XML-escaping of the element: ampersand, angle brackets, both // quote styles and a non-BMP code point. { id: 'title-escaping', options: { seed: 'parity-2', title: 'Tom & Jerry\'s <"Avatar"> ' + EMOJI, }, }, ...extra, ]; } const avatarFixtures = { initials: avatarCases([ { id: 'font-family-weight', options: { seed: 'Florian Körner', fontFamily: ['Helvetica', 'Arial'], fontWeight: 700, }, }, { id: 'variant-double', options: { seed: 'AB CD', initialsVariant: ['double'] }, }, { id: 'variant-single', options: { seed: 'AB CD', initialsVariant: ['single'] }, }, ]), thumbs: avatarCases([ { id: 'eyes-mouth-variant', options: { seed: 'parity-1', eyesVariant: ['variant1W10'], mouthVariant: ['variant1'], }, }, { id: 'shape-color-override', options: { seed: 'parity-1', shapeColor: ['ffaa00'], eyesColor: ['000000'], mouthColor: ['ff0000'], }, }, ]), glass: avatarCases([ { id: 'shape-variants', options: { seed: 'parity-1', shape1Variant: ['a'], shape2Variant: ['g'], }, }, { id: 'shape1-probability-zero', options: { seed: 'parity-1', shape1Probability: 0 }, }, ]), notionists: avatarCases([ { id: 'hair-eyes-variant', options: { seed: 'parity-1', hairVariant: ['variant01'], eyesVariant: ['variant01'], }, }, { id: 'gesture-variant', options: { seed: 'parity-1', gestureVariant: ['waveLongArms'], }, }, ]), 'shape-grid': avatarCases([ { id: 'rows-variant-pinned', options: { seed: 'parity-1', rowsVariant: ['rows04'] }, }, { id: 'shape-variant-pinned', options: { seed: 'parity-1', shapeVariant: ['square'] }, }, { id: 'shape-probability-zero', options: { seed: 'parity-1', shapeProbability: 0 }, }, ]), }; for (const [styleName, cases] of Object.entries(avatarFixtures)) { const styleData = styles[styleName]; const out = cases.map((c) => { const avatar = new Avatar(styleData, c.options); const json = avatar.toJSON(); // JSON-round-trip the resolved options so the fixture matches what // any consumer would see after JSON.stringify — this drops undefined // values like `size` and `title` when they were not provided. const resolvedOptions = JSON.parse(JSON.stringify(json.options)); const result = { id: c.id, options: c.options, svg: json.svg, resolvedOptions }; // Pins the percent-encoding contract (JS `encodeURIComponent`: everything // except `A-Za-z0-9-_.!~*'()` is escaped). Two cases per style suffice — // the encoder is byte-level, so `plain-seed` (typical SVG markup) plus // `title-escaping` (escaped XML + multi-byte UTF-8) cover the alphabet // without doubling every fixture entry. if (DATA_URI_CASE_IDS.has(c.id)) { result.dataUri = avatar.toDataUri(); } return result; }); writeJson(join('avatars', `${styleName}.json`), out); } // --------------------------------------------------------------------------- // OptionsDescriptor fixtures // --------------------------------------------------------------------------- // // One descriptor per parity style. Pins the field map (types, ranges, sorted // variant lists, per-color fields) that tooling builds form controls from. console.log('Generating descriptor fixtures…'); for (const [styleName, styleData] of Object.entries(styles)) { const descriptor = new OptionsDescriptor(new Style(styleData)).toJSON(); writeJson(join('descriptors', `${styleName}.json`), descriptor); } console.log('Done.');