diff --git a/syncify/options/define/sections.ts b/syncify/options/define/sections.ts index 1463e151..bfde8f2e 100644 --- a/syncify/options/define/sections.ts +++ b/syncify/options/define/sections.ts @@ -121,142 +121,176 @@ async function setSchemaJson () { const { shared } = $.section; const warn = warnOption('Section Schema'); - const files = [ ...$.paths.blocks.input, ...$.paths.sections.input ]; + const files = [ ...$.paths.blocks.input, ...$.paths.sections.input, ...$.paths.config.input ]; - for (const file of files) { + function buildSettingsCache (file: string, settings: SchemaSettings[]) { - const read = await readFile(file, 'utf8'); - const hash = checksum(read); + const refs = []; - if (has(file, $.cache.schema) && $.cache.checksum[file] === hash) continue; + for (const setting of settings) { - $.cache.checksum[file] = hash; + if (has('$ref', setting)) { - const data = read.toString(); - const indices = GetSchemaIndices(data); + const [ key, prop ] = setting.$ref.split('.'); - if (indices === null) { - warn('Liquid Parse Error', relative($.cwd, file)); - continue; - } + if (shared.has(key)) { - try { + if (!$.cache.schema[shared.get(key).uri].has(file)) { + + $.cache.schema[shared.get(key).uri].add(file); + + if (has('settings', shared.get(key).schema[prop])) { + + refs.push((shared.get(key).schema[prop] as SettingsGroup).settings); - const schema = parse(data.slice(indices.begin, indices.ender)); - const schemaProp = hasProp(schema); + continue; - function buildSettingsCache (file: string, settings: SchemaSettings[]) { + } + + refs.push(shared.get(key).schema[prop]); + + } - const refs = []; + } - for (const setting of settings) { + } + } - if (has('$ref', setting)) { + for (const settings of refs) { - const [ key, prop ] = setting.$ref.split('.'); + buildSettingsCache(file, settings); - if (shared.has(key)) { + } - if (!$.cache.schema[shared.get(key).uri].has(file)) { + } - $.cache.schema[shared.get(key).uri].add(file); + function buildBlockCache (file: string, blocks: SchemaBlocks[]) { - if (has('settings', shared.get(key).schema[prop])) { + const blockRefs: SchemaBlocks[] = []; + const settingsRefs = []; - refs.push((shared.get(key).schema[prop] as SettingsGroup).settings); + for (const block of blocks) { - continue; + const blockProp = hasProp(block); - } + if (blockProp('$ref')) { - refs.push(shared.get(key).schema[prop]); + const [ key, prop ] = block.$ref.split('.'); - } + if (shared.has(key)) { - } + if (!$.cache.schema[shared.get(key).uri].has(file)) { - } - } + $.cache.schema[shared.get(key).uri].add(file); - for (const settings of refs) { + blockRefs.push(shared.get(key).schema[prop] as SchemaBlocks); - buildSettingsCache(file, settings); + }; } } - function buildBlockCache (file: string, blocks: SchemaBlocks[]) { + if (blockProp('settings')) { - const blockRefs: SchemaBlocks[] = []; - const settingsRefs = []; + settingsRefs.push(block.settings); - for (const block of blocks) { + } + } - const blockProp = hasProp(block); + for (const block of blockRefs) { - if (blockProp('$ref')) { + buildBlockCache(file, [ block ]); - const [ key, prop ] = block.$ref.split('.'); + } - if (shared.has(key)) { + for (const settings of settingsRefs) { - if (!$.cache.schema[shared.get(key).uri].has(file)) { + buildSettingsCache(file, settings); - $.cache.schema[shared.get(key).uri].add(file); + } - blockRefs.push(shared.get(key).schema[prop] as SchemaBlocks); + } - }; + for (const file of files) { - } + const read = await readFile(file, 'utf8'); + const hash = checksum(read); - } + if (has(file, $.cache.schema) && $.cache.checksum[file] === hash) continue; - if (blockProp('settings')) { + $.cache.checksum[file] = hash; - settingsRefs.push(block.settings); + const data = read.toString(); - } - } + if (file.includes('/config/') && file.includes('settings_schema.json')) { - for (const block of blockRefs) { + const schema = parse(data); - buildBlockCache(file, [ block ]); + schema.slice(1).forEach((setting: {name: string, settings: any[]}) => { + if (has('settings', setting) && isArray(setting.settings)) { - } + try { - for (const settings of settingsRefs) { + buildSettingsCache(file, setting.settings); - buildSettingsCache(file, settings); + } catch (e) { + + if (has(file, $.cache.sections)) { + + delete $.cache.sections[file]; + + } + + warn('Config Parse Error', relative($.cwd, file)); + } } + }); - } + continue; - if (schemaProp('settings')) { + } + + if (file.includes('/sections/') || file.includes('/blocks/')) { - buildSettingsCache(file, schema.settings); + const indices = GetSchemaIndices(data); + if (indices === null) { + warn('Liquid Parse Error', relative($.cwd, file)); + continue; } - if (schemaProp('blocks')) { + try { - buildBlockCache(file, schema.blocks); + const schema = parse(data.slice(indices.begin, indices.ender)); + const schemaProp = hasProp(schema); - } + if (schemaProp('settings')) { - } catch (e) { + buildSettingsCache(file, schema.settings); - if (has(file, $.cache.sections)) { + } - delete $.cache.sections[file]; + if (schemaProp('blocks')) { - } + buildBlockCache(file, schema.blocks); - warn('JSON Parse Error', relative($.cwd, file)); + } - } + } catch (e) { + + if (has(file, $.cache.sections)) { + + delete $.cache.sections[file]; + + } + + warn('JSON Parse Error', relative($.cwd, file)); + + } + + }; } diff --git a/syncify/transform/json.ts b/syncify/transform/json.ts index 6ac4e47a..8f555f80 100644 --- a/syncify/transform/json.ts +++ b/syncify/transform/json.ts @@ -4,7 +4,7 @@ import { join } from 'node:path'; import { readFile, writeFile } from 'fs-extra'; -import { evaluate, ParseEvaluate, stringify } from '@syncify/json'; +import { evaluate, parse, ParseEvaluate, stringify } from '@syncify/json'; import { timer } from '@syncify/timer'; import { log } from '~cli/log'; @@ -14,6 +14,7 @@ import { themeFilesGet, themeFilesUpsertMap } from '~http/themeFiles'; import { runChecksum } from '~process/cache'; import { prompt } from '~prompt'; import { theme } from '~prompts/enquirer'; +import { InjectSettings } from '~schema'; import { tailwindParse } from '~style'; import * as u from '~utils'; @@ -261,7 +262,7 @@ export async function JsonTransform (file: File): Promise { if (!u.isString(read)) return; - const local = read.trim(); + let local = read.trim(); file.size = u.byteSize(local); @@ -270,8 +271,34 @@ export async function JsonTransform (file: File): Promise { return; } + if (file.name === 'settings_schema') { + + const schemaFiles = u.values($.cache.schema); + + for (const schemaFile of schemaFiles) { + schemaFile.delete(file.input); + } + + const settings = parse(local); + + settings.slice(1).forEach((schema: {name: string, settings: any[]}) => { + if (u.has('settings', schema) && u.isArray(schema.settings)) { + schema.settings = InjectSettings(file, schema.settings); + } + }); + + local = stringify(settings); + + } + if ($.mode.build === false && isDiff(file.type)) { + + if (file.name === 'settings_schema') { + file.value = await jsonCompile(file, local); + } + file.value = await jsonCompare(file, local); + } else { file.value = await jsonCompile(file, local); } diff --git a/syncify/transform/schema.ts b/syncify/transform/schema.ts index e57283f6..784411b1 100644 --- a/syncify/transform/schema.ts +++ b/syncify/transform/schema.ts @@ -23,8 +23,9 @@ import { log } from '~cli/log'; import { warn } from '~cli/warnings'; import { error } from '~errors'; import { File, Type } from '~file'; +import { JsonTransform } from '~json'; import { LiquidTransform } from '~liquid'; -import { checksum, defineProperty, has, hasProp, includes, isArray, isEmpty, isNull, isObject, isString, merge, o, omit, plur, replaceAllOccurrences, s, toArray, values } from '~utils'; +import { checksum, defineProperty, has, hasProp, isArray, isEmpty, isNull, isObject, isString, merge, o, omit, plur, replaceAllOccurrences, s, toArray, values } from '~utils'; import { $, q } from '$'; @@ -177,6 +178,107 @@ export async function ExtractSchema (file: File): Promise<[ } +/** + * Properties relating to sidebar settings. + */ +export const sidebarProps = [ + 'content' +]; + +/** + * Properties common to all schema setting types, derived from `Schema.Common`. + */ +export const commonProps = [ + 'id', + 'label', + 'info' +]; + +/** + * Specific properties setting types, derived from `Schema.Common`. + */ +const headerProps: string[] = []; +const paragraphProps = [ 'info' ]; +const checkboxProps = [ 'default' ]; +const numberProps = [ 'default', 'placeholder' ]; +const radioProps = [ 'options', 'default' ]; +const rangeProps = [ 'min', 'max', 'step', 'unit', 'default' ]; +const selectProps = [ 'options', 'default' ]; +const textProps = [ 'placeholder', 'default' ]; +const textareaProps = [ 'placeholder', 'default' ]; +const articleProps: string[] = []; +const blogProps: string[] = []; +const collectionProps: string[] = []; +const imagePickerProps: string[] = []; +const videoProps: string[] = []; +const pageProps: string[] = []; +const productProps: string[] = []; +const collectionListProps = [ 'limit' ]; +const colorProps = [ 'default' ]; +const colorBackgroundProps = [ 'default' ]; +const fontPickerProps = [ 'default' ]; +const htmlProps = [ 'placeholder', 'default' ]; +const inlineRichTextProps = [ 'default' ]; +const linkListProps = [ 'default' ]; +const liquidProps = [ 'default' ]; +const productListProps = [ 'limit' ]; +const richTextProps = [ 'default' ]; +const urlProps = [ 'default' ]; +const videoUrlProps = [ 'placeholder', 'accept' ]; + +/** + * A map that provides an array of all allowed property names for a given + * schema setting 'type'. It combines the common properties with the + * type-specific properties. + * + * @example + * const propsForRange = allowedPropsMap.range; + */ +export const allowedPropsMap: Record = { + // Sidebar Inputs + header: [ ...sidebarProps, ...headerProps ], + paragraph: [ ...sidebarProps, ...paragraphProps ], + + // Basic Inputs + checkbox: [ ...commonProps, ...checkboxProps ], + number: [ ...commonProps, ...numberProps ], + radio: [ ...commonProps, ...radioProps ], + range: [ ...commonProps, ...rangeProps ], + select: [ ...commonProps, ...selectProps ], + text: [ ...commonProps, ...textProps ], + textarea: [ ...commonProps, ...textareaProps ], + + // Specialized Inputs + article: [ ...commonProps, ...articleProps ], + blog: [ ...commonProps, ...blogProps ], + collection: [ ...commonProps, ...collectionProps ], + collection_list: [ ...commonProps, ...collectionListProps ], + color: [ ...commonProps, ...colorProps ], + color_background: [ ...commonProps, ...colorBackgroundProps ], + font_picker: [ ...commonProps, ...fontPickerProps ], + html: [ ...commonProps, ...htmlProps ], + image_picker: [ ...commonProps, ...imagePickerProps ], + inline_richtext: [ ...commonProps, ...inlineRichTextProps ], + link_list: [ ...commonProps, ...linkListProps ], + liquid: [ ...commonProps, ...liquidProps ], + page: [ ...commonProps, ...pageProps ], + product: [ ...commonProps, ...productProps ], + product_list: [ ...commonProps, ...productListProps ], + richtext: [ ...commonProps, ...richTextProps ], + url: [ ...commonProps, ...urlProps ], + video: [ ...commonProps, ...videoProps ], + video_url: [ ...commonProps, ...videoUrlProps ] +}; + +/** + * A single array containing all unique property names available across all + * setting types. This is useful for validating properties or filtering + * out any keys that are not valid for any setting. + */ +export const allAvailableSettingProps = [ + ...new Set(Object.values(allowedPropsMap).flat()) +]; + /** * Overrides Builder * @@ -199,13 +301,133 @@ export function OverridesBuilder ( type: 'block' | 'setting' = 'setting' ) { - let allowedProps: string[] = []; + const allowedProps: string[] = []; if (type === 'setting') { schema = schema as SchemaSettings; - allowedProps.push('id', 'label', 'info', 'visible_if', 'default', 'options', 'placeholder'); + switch (true) { + case has('$ref', schema): + allowedProps.push(...allAvailableSettingProps); + break; + + case schema.type === 'header': + allowedProps.push(...allowedPropsMap.header); + break; + + case schema.type === 'paragraph': + allowedProps.push(...allowedPropsMap.paragraph); + break; + + case schema.type === 'checkbox': + allowedProps.push(...allowedPropsMap.checkbox); + break; + + case schema.type === 'number': + allowedProps.push(...allowedPropsMap.number); + break; + + case schema.type === 'radio': + allowedProps.push(...allowedPropsMap.radio); + break; + + case schema.type === 'range': + allowedProps.push(...allowedPropsMap.range); + break; + + case schema.type === 'select': + allowedProps.push(...allowedPropsMap.select); + break; + + case schema.type === 'text': + allowedProps.push(...allowedPropsMap.text); + break; + + case schema.type === 'textarea': + allowedProps.push(...allowedPropsMap.textarea); + break; + + case schema.type === 'article': + allowedProps.push(...allowedPropsMap.article); + break; + + case schema.type === 'blog': + allowedProps.push(...allowedPropsMap.blog); + break; + + case schema.type === 'collection': + allowedProps.push(...allowedPropsMap.collection); + break; + + case schema.type === 'collection_list': + allowedProps.push(...allowedPropsMap.collection_list); + break; + + case schema.type === 'color': + allowedProps.push(...allowedPropsMap.color); + break; + + case schema.type === 'color_background': + allowedProps.push(...allowedPropsMap.color_background); + break; + + case schema.type === 'font_picker': + allowedProps.push(...allowedPropsMap.font_picker); + break; + + case schema.type === 'html': + allowedProps.push(...allowedPropsMap.html); + break; + + case schema.type === 'image_picker': + allowedProps.push(...allowedPropsMap.image_picker); + break; + + case schema.type === 'inline_richtext': + allowedProps.push(...allowedPropsMap.inline_richtext); + break; + + case schema.type === 'link_list': + allowedProps.push(...allowedPropsMap.link_list); + break; + + case schema.type === 'liquid': + allowedProps.push(...allowedPropsMap.liquid); + break; + + case schema.type === 'page': + allowedProps.push(...allowedPropsMap.page); + break; + + case schema.type === 'product': + allowedProps.push(...allowedPropsMap.product); + break; + + case schema.type === 'product_list': + allowedProps.push(...allowedPropsMap.product_list); + break; + + case schema.type === 'richtext': + allowedProps.push(...allowedPropsMap.richtext); + break; + + case schema.type === 'url': + allowedProps.push(...allowedPropsMap.url); + break; + + case schema.type === 'video': + allowedProps.push(...allowedPropsMap.video); + break; + + case schema.type === 'video_url': + allowedProps.push(...allowedPropsMap.video_url); + break; + + default: + allowedProps.push(...commonProps); + break; + } if (has('_settings', schema)) { schema = merge(schema, schema._settings); @@ -236,17 +458,12 @@ export function OverridesBuilder ( for (const [ key, value ] of Object.entries(schema)) { - if (key === 'type' && value !== 'select' && value !== 'radio' && includes('options', allowedProps)) { - allowedProps = allowedProps.filter(function (item) { - return item !== 'options'; - }); + for (const [ key ] of Object.entries(overrides)) { - delete overrides.options; - } + if (!s(allowedProps).has(key)) { + delete overrides[key]; + } - if (!s(allowedProps).has(key)) { - delete overrides[key]; - continue; } if (isNull(value) || isNull(overrides[key])) { @@ -259,7 +476,6 @@ export function OverridesBuilder ( } if (key === 'visible_if') { - // This mixes liquid in a string so could be dangerous searching for wildcard '*' values continue; } @@ -826,7 +1042,20 @@ export async function SchemaTransform (file: File) { log.nl(); for (const file of files) { - await LiquidTransform(file); + switch (file.type) { + + case Type.Section: + case Type.Block: + + await LiquidTransform(file); + break; + + case Type.Config: + + await JsonTransform(file); + break; + + } } if ($.mode.hot && $.mode.bulk === false) {