From 6f90bcc66f815a2c8e22a6069729aee8627b4caa Mon Sep 17 00:00:00 2001 From: Ariba Rajput Date: Thu, 5 Dec 2024 16:20:30 -0500 Subject: [PATCH] Reworked --- .changeset/chilled-bugs-juggle.md | 2 +- .../theme-check-common/src/checks/index.ts | 4 +- .../valid-block-preset-settings/index.spec.ts | 166 --- .../valid-block-preset-settings/index.ts | 116 -- .../valid-preset-settings/index.spec.ts | 1096 +++++++++++++++++ .../src/checks/valid-preset-settings/index.ts | 151 +++ packages/theme-check-node/configs/all.yml | 2 +- .../theme-check-node/configs/recommended.yml | 2 +- 8 files changed, 1252 insertions(+), 287 deletions(-) delete mode 100644 packages/theme-check-common/src/checks/valid-block-preset-settings/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/valid-block-preset-settings/index.ts create mode 100644 packages/theme-check-common/src/checks/valid-preset-settings/index.spec.ts create mode 100644 packages/theme-check-common/src/checks/valid-preset-settings/index.ts diff --git a/.changeset/chilled-bugs-juggle.md b/.changeset/chilled-bugs-juggle.md index 4373b37eb..379aede89 100644 --- a/.changeset/chilled-bugs-juggle.md +++ b/.changeset/chilled-bugs-juggle.md @@ -3,4 +3,4 @@ 'theme-check-vscode': minor --- -Add the `ValidBlockPresetSettings` check. +Add the `ValidPresetSettings` check. This check will allow us to validate that the settings defined in sections and blocks are valid. They are valid if they exist in either the block settings or the section settings. diff --git a/packages/theme-check-common/src/checks/index.ts b/packages/theme-check-common/src/checks/index.ts index e0803940a..1bc31d891 100644 --- a/packages/theme-check-common/src/checks/index.ts +++ b/packages/theme-check-common/src/checks/index.ts @@ -45,7 +45,7 @@ import { ValidSchemaName } from './valid-schema-name'; import { ValidStaticBlockType } from './valid-static-block-type'; import { VariableName } from './variable-name'; import { MissingSchema } from './missing-schema'; -import { ValidBlockPresetSettings } from './valid-block-preset-settings'; +import { ValidPresetSettings } from './valid-preset-settings'; export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [ AppBlockValidTags, @@ -93,7 +93,7 @@ export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [ ValidStaticBlockType, VariableName, ValidSchemaName, - ValidBlockPresetSettings, + ValidPresetSettings, ]; /** diff --git a/packages/theme-check-common/src/checks/valid-block-preset-settings/index.spec.ts b/packages/theme-check-common/src/checks/valid-block-preset-settings/index.spec.ts deleted file mode 100644 index 944dee033..000000000 --- a/packages/theme-check-common/src/checks/valid-block-preset-settings/index.spec.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { ValidBlockPresetSettings } from '.'; -import { check } from '../../test/test-helper'; -import { MockTheme } from '../../test/MockTheme'; - -describe('ValidBlockPresetSettings', () => { - it('should report invalid preset settings', async () => { - const theme: MockTheme = { - 'blocks/price.liquid': ` - {% schema %} - { - "name": "t:names.product_price", - "settings": [ - { - "type": "product", - "id": "product", - "label": "t:settings.product" - }, - ], - "presets": [ - { - "name": "t:names.product_price", - "settings": { - "product": "{{ context.product }}", - "undefined_setting": "some value", - } - } - ] - } - {% endschema %} - `, - }; - const offenses = await check(theme, [ValidBlockPresetSettings]); - expect(offenses).to.have.length(1); - }); - - it('should report invalid theme block preset settings', async () => { - const theme: MockTheme = { - 'blocks/block_1.liquid': ` - {% schema %} - { - "name": "t:names.block_1", - "settings": [ - { - "type": "text", - "id": "block_1_setting_key", - "label": "t:settings.block_1" - }, - ] - } - {% endschema %} - `, - 'blocks/price.liquid': ` - {% schema %} - { - "name": "t:names.product_price", - "settings": [ - { - "type": "product", - "id": "product", - "label": "t:settings.product" - } - ], - "blocks": [ - { - "type": "block_1", - "name": "t:names.block_1", - } - ], - "presets": [ - { - "name": "t:names.product_price", - "settings": { - "product": "{{ context.product }}", - }, - "blocks": [ - { - "block_1": { - "type": "block_1", - "settings": { - "block_1_setting_key": "correct setting key", - "undefined_setting": "incorrect setting key" - } - } - } - ], - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockPresetSettings]); - expect(offenses).to.have.length(1); - expect(offenses[0].message).to.include( - 'Preset setting "undefined_setting" does not exist in the block type "block_1"\'s settings', - ); - }); - - it('should not report when all section and block preset settings are valid', async () => { - const theme: MockTheme = { - 'blocks/block_1.liquid': ` - {% schema %} - { - "name": "t:names.block_1", - "settings": [ - { - "type": "text", - "id": "block_1_setting_key", - "label": "t:settings.block_1" - } - ] - } - {% endschema %} - `, - 'blocks/price.liquid': ` - {% schema %} - { - "name": "t:names.product_price", - "settings": [ - { - "type": "product", - "id": "product", - "label": "t:settings.product" - }, - { - "type": "text", - "id": "section_setting", - "label": "t:settings.section" - } - ], - "blocks": [ - { - "type": "block_1", - "name": "t:names.block_1" - } - ], - "presets": [ - { - "name": "t:names.product_price", - "settings": { - "product": "{{ context.product }}", - "section_setting": "some value" - }, - "blocks": [ - { - "block_1": { - "type": "block_1", - "settings": { - "block_1_setting_key": "correct setting key" - } - } - } - ] - } - ] - } - {% endschema %} - `, - }; - - const offenses = await check(theme, [ValidBlockPresetSettings]); - expect(offenses).to.have.length(0); - }); -}); diff --git a/packages/theme-check-common/src/checks/valid-block-preset-settings/index.ts b/packages/theme-check-common/src/checks/valid-block-preset-settings/index.ts deleted file mode 100644 index faafba793..000000000 --- a/packages/theme-check-common/src/checks/valid-block-preset-settings/index.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { isSection, isBlock } from '../../to-schema'; -import { basename } from '../../path'; -import { LiquidCheckDefinition, Severity, SourceCodeType, ThemeBlock } from '../../types'; -import { Preset } from '../../types/schemas/preset'; -import { Setting } from '../../types/schemas/setting'; - -export const ValidBlockPresetSettings: LiquidCheckDefinition = { - meta: { - code: 'ValidBlockPresetSettings', - name: 'Reports invalid preset settings for a theme block', - docs: { - description: 'Reports invalid preset settings for a theme block', - recommended: true, - url: 'https://shopify.dev/docs/storefronts/themes/tools/theme-check/checks/valid-block-preset-settings', - }, - severity: Severity.ERROR, - type: SourceCodeType.LiquidHtml, - schema: {}, - targets: [], - }, - - create(context) { - function getSchema() { - const name = basename(context.file.uri, '.liquid'); - switch (true) { - case isBlock(context.file.uri): - return context.getBlockSchema?.(name); - case isSection(context.file.uri): - return context.getSectionSchema?.(name); - default: - return undefined; - } - } - - function getInlineSettingsTypesAndKeys(settings: Setting.Any[]) { - if (!settings) return []; - return settings.map((setting: { id: any; type: any }) => ({ - id: setting.id, - type: setting.type, - })); - } - - function getPresetSettingsKeys(presets: Preset.Preset[]) { - const allKeys: string[] = []; - for (const preset of presets) { - if (preset.settings) { - allKeys.push(...Object.keys(preset.settings)); - } - } - return allKeys; - } - - function getPresetBlockSettingsKeys(blocks: Preset.PresetBlocks) { - const allKeys: string[] = []; - for (const block of Object.values(blocks)) { - for (const [_, blockData] of Object.entries(block)) { - if (blockData && typeof blockData === 'object' && 'settings' in blockData) { - const settings = blockData.settings; - if (settings && typeof settings === 'object') { - allKeys.push(...Object.keys(settings)); - } - } - } - } - return allKeys; - } - - return { - async LiquidRawTag(node) { - if (node.name !== 'schema' || node.body.kind !== 'json') { - return; - } - - const schema = await getSchema(); - if (!schema) return; - if (schema.validSchema instanceof Error) return; - - const validSchema = schema.validSchema; - const settingsKeys = getInlineSettingsTypesAndKeys(validSchema.settings); - const presetSettingsKeys = getPresetSettingsKeys(validSchema.presets ?? []); - - for (const key of presetSettingsKeys) { - if (!settingsKeys.some((setting) => setting.id === key)) { - context.report({ - message: `Preset setting "${key}" does not exist in the block's settings`, - startIndex: 0, - endIndex: 0, - }); - } - } - - if (validSchema.blocks) { - for (const block of validSchema.blocks) { - const blockSchema = await context.getBlockSchema?.(block.type); - if (!blockSchema || blockSchema.validSchema instanceof Error) continue; - - for (const preset of validSchema.presets ?? []) { - if (!preset.blocks) continue; - const presetBlockSettingKeys = getPresetBlockSettingsKeys(preset.blocks) ?? []; - - for (const key of presetBlockSettingKeys) { - if (!blockSchema.validSchema.settings?.some((setting) => setting.id === key)) { - context.report({ - message: `Preset setting "${key}" does not exist in the block type "${block.type}"'s settings`, - startIndex: 0, - endIndex: 0, - }); - } - } - } - } - } - }, - }; - }, -}; diff --git a/packages/theme-check-common/src/checks/valid-preset-settings/index.spec.ts b/packages/theme-check-common/src/checks/valid-preset-settings/index.spec.ts new file mode 100644 index 000000000..bddd0c2f2 --- /dev/null +++ b/packages/theme-check-common/src/checks/valid-preset-settings/index.spec.ts @@ -0,0 +1,1096 @@ +import { describe, expect, it } from 'vitest'; +import { ValidPresetSettings } from '.'; +import { check } from '../../test/test-helper'; +import { MockTheme } from '../../test/MockTheme'; + +describe('ValidPresetSettings', () => { + it('should report invalid keys in a blocks preset setting', async () => { + const theme: MockTheme = { + 'blocks/price.liquid': ` + {% schema %} + { + "name": "t:names.product_price", + "settings": [ + { + "type": "product", + "id": "product", + "label": "t:settings.product" + }, + { + "type": "collection", + "id": "collection", + "label": "t:settings.collection" + } + ], + "presets": [ + { + "name": "t:names.product_price", + "settings": { + "product": "{{ context.product }}", + "undefined_setting": "some value" + } + } + ] + } + {% endschema %} + `, + }; + + const offenses = await check(theme, [ValidPresetSettings]); + expect(offenses).to.have.length(1); + expect(offenses[0].message).to.include( + 'Preset setting "undefined_setting" does not exist in settings', + ); + }); + + it('should report invalid keys in a blocks nested block preset setting', async () => { + const theme: MockTheme = { + 'blocks/block_1.liquid': ` + {% schema %} + { + "name": "t:names.block_1", + "settings": [ + { + "type": "text", + "id": "block_1_setting_key", + "label": "t:settings.block_1" + } + ] + } + {% endschema %} + `, + 'blocks/price.liquid': ` + {% schema %} + { + "name": "t:names.product_price", + "settings": [ + { + "type": "product", + "id": "product", + "label": "t:settings.product" + }, + { + "type": "collection", + "id": "collection", + "label": "t:settings.collection" + } + ], + "blocks": [ + { + "type": "block_1", + "name": "t:names.block_1" + } + ], + "presets": [ + { + "name": "t:names.product_price", + "settings": { + "product": "{{ context.product }}", + "collection": "{{ context.collection }}" + }, + "blocks": [ + { + "block_1": { + "type": "block_1", + "settings": { + "block_1_setting_key": "correct setting key", + "undefined_setting": "incorrect setting key" + } + } + } + ] + }, + { + "name": "t:names.product_price_2", + "settings": { + "product": "{{ context.product }}", + "collection": "{{ context.collection }}" + } + } + ] + } + {% endschema %} + `, + }; + + const offenses = await check(theme, [ValidPresetSettings]); + expect(offenses).to.have.length(1); + expect(offenses[0].message).to.include( + `Preset block setting "undefined_setting" does not exist in settings`, + ); + }); + + it('should not report when all preset settings in the block are valid', async () => { + const theme: MockTheme = { + 'blocks/block_1.liquid': ` + {% schema %} + { + "name": "t:names.block_1", + "settings": [ + { + "type": "text", + "id": "block_1_setting_key", + "label": "t:settings.block_1" + } + ] + } + {% endschema %} + `, + 'blocks/price.liquid': ` + {% schema %} + { + "name": "t:names.product_price", + "settings": [ + { + "type": "product", + "id": "product", + "label": "t:settings.product" + }, + { + "type": "text", + "id": "section_setting", + "label": "t:settings.section" + } + ], + "blocks": [ + { + "type": "block_1", + "name": "t:names.block_1" + } + ], + "presets": [ + { + "name": "t:names.product_price", + "settings": { + "product": "{{ context.product }}", + "section_setting": "some value" + }, + "blocks": [ + { + "block_1": { + "type": "block_1", + "settings": { + "block_1_setting_key": "correct setting key" + } + } + } + ] + } + ] + } + {% endschema %} + `, + }; + + const offenses = await check(theme, [ValidPresetSettings]); + expect(offenses).to.have.length(0); + }); + + it('should report invalid keys in a sections preset setting', async () => { + const theme: MockTheme = { + 'sections/header-announcements.liquid': ` + {% schema %} + { + "name": "Announcement bar", + "tag": "aside", + "blocks": [ + { + "type": "announcement" + } + ], + "enabled_on": { + "groups": [ + "header" + ] + }, + "settings": [ + { + "type": "select", + "id": "show_as", + "label": "Type", + "options": [ + { + "value": "carousel", + "label": "Carousel" + }, + { + "value": "list", + "label": "List" + }, + { + "value": "scroll", + "label": "Scroll" + } + ] + }, + { + "type": "checkbox", + "id": "auto_rotate", + "label": "Auto rotate", + "default": true, + "available_if": "{{ section.settings.show_as == 'carousel' }}" + }, + { + "type": "select", + "id": "align_items", + "label": "Alignment", + "options": [ + { + "value": "start", + "label": "Start" + }, + { + "value": "center", + "label": "Center" + }, + { + "value": "end", + "label": "End" + } + ], + "default": "center", + "available_if": "{{ section.settings.show_as == 'list' }}" + }, + { + "type": "range", + "id": "gap", + "label": "Gap", + "min": 0, + "max": 100, + "unit": "px", + "default": 16, + "available_if": "{{ section.settings.show_as == 'list' }}" + }, + { + "type": "range", + "id": "speed", + "label": "Speed", + "min": 0, + "max": 5, + "default": 5, + "unit": "sec", + "available_if": "{{ section.settings.show_as == 'scroll' }}" + }, + { + "type": "color_scheme", + "id": "color_scheme", + "default": "scheme-4", + "label": "Color Scheme" + }, + { + "type": "header", + "content": "Padding" + }, + { + "type": "range", + "id": "padding-block-start", + "label": "Top", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 16 + }, + { + "type": "range", + "id": "padding-block-end", + "label": "Bottom", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 16 + }, + { + "type": "range", + "id": "padding-inline-start", + "label": "Left", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 2 + }, + { + "type": "range", + "id": "padding-inline-end", + "label": "Right", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 2 + }, + { + "type": "header", + "content": "Margin" + }, + { + "type": "range", + "id": "margin-block-start", + "label": "Top", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 0 + }, + { + "type": "range", + "id": "margin-block-end", + "label": "Bottom", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 0 + }, + { + "type": "range", + "id": "margin-inline-start", + "label": "Left", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 0 + }, + { + "type": "range", + "id": "margin-inline-end", + "label": "Right", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 0 + } + ], + "presets": [ + { + "name": "Announcement bar", + "settings": { + "undefined_setting": "list" + } + } + ] + } + {% endschema %} + `, + }; + + const offenses = await check(theme, [ValidPresetSettings]); + expect(offenses).to.have.length(1); + expect(offenses[0].message).to.include( + `Preset setting "undefined_setting" does not exist in settings`, + ); + }); + + it('should report invalid keys in a sections nested block preset setting', async () => { + const theme: MockTheme = { + 'blocks/announcement.liquid': ` + {% schema %} + { + "name": "Announcement", + "settings": [ + { + "type": "text", + "id": "text", + "label": "Text" + } + ] + } + {% endschema %} + `, + 'sections/header-announcements.liquid': ` + {% schema %} + { + "name": "Announcement bar", + "tag": "aside", + "blocks": [ + { + "type": "announcement" + } + ], + "enabled_on": { + "groups": [ + "header" + ] + }, + "settings": [ + { + "type": "select", + "id": "show_as", + "label": "Type", + "options": [ + { + "value": "carousel", + "label": "Carousel" + }, + { + "value": "list", + "label": "List" + }, + { + "value": "scroll", + "label": "Scroll" + } + ] + }, + { + "type": "checkbox", + "id": "auto_rotate", + "label": "Auto rotate", + "default": true, + "available_if": "{{ section.settings.show_as == 'carousel' }}" + }, + { + "type": "select", + "id": "align_items", + "label": "Alignment", + "options": [ + { + "value": "start", + "label": "Start" + }, + { + "value": "center", + "label": "Center" + }, + { + "value": "end", + "label": "End" + } + ], + "default": "center", + "available_if": "{{ section.settings.show_as == 'list' }}" + }, + { + "type": "range", + "id": "gap", + "label": "Gap", + "min": 0, + "max": 100, + "unit": "px", + "default": 16, + "available_if": "{{ section.settings.show_as == 'list' }}" + }, + { + "type": "range", + "id": "speed", + "label": "Speed", + "min": 0, + "max": 5, + "default": 5, + "unit": "sec", + "available_if": "{{ section.settings.show_as == 'scroll' }}" + }, + { + "type": "color_scheme", + "id": "color_scheme", + "default": "scheme-4", + "label": "Color Scheme" + }, + { + "type": "header", + "content": "Padding" + }, + { + "type": "range", + "id": "padding-block-start", + "label": "Top", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 16 + }, + { + "type": "range", + "id": "padding-block-end", + "label": "Bottom", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 16 + }, + { + "type": "range", + "id": "padding-inline-start", + "label": "Left", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 2 + }, + { + "type": "range", + "id": "padding-inline-end", + "label": "Right", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 2 + }, + { + "type": "header", + "content": "Margin" + }, + { + "type": "range", + "id": "margin-block-start", + "label": "Top", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 0 + }, + { + "type": "range", + "id": "margin-block-end", + "label": "Bottom", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 0 + }, + { + "type": "range", + "id": "margin-inline-start", + "label": "Left", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 0 + }, + { + "type": "range", + "id": "margin-inline-end", + "label": "Right", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 0 + } + ], + "presets": [ + { + "name": "Announcement bar", + "blocks": [ + { + "announcement": { + "type": "announcement", + "settings": { + "undefined_setting": "list" + } + } + } + ] + } + ] + } + {% endschema %} + `, + }; + + const offenses = await check(theme, [ValidPresetSettings]); + expect(offenses).to.have.length(1); + expect(offenses[0].message).to.include( + `Preset block setting "undefined_setting" does not exist in settings`, + ); + }); + + it('should not report when all preset settings in the section are valid', async () => { + const theme: MockTheme = { + 'blocks/group.liquid': ` + {% schema %} +{ + "name": "Group", + "tag": null, + "blocks": [{ "type": "@theme" }, { "type": "@app" }, { "type": "_divider" }], + "settings": [ + { + "type": "header", + "content": "Layout" + }, + { + "type": "select", + "id": "layout_style", + "label": "Type", + "options": [ + { + "value": "flex", + "label": "Stack" + }, + { + "value": "grid", + "label": "Grid" + } + ], + "default": "flex" + }, + { + "type": "select", + "id": "content_direction", + "label": "Direction", + "options": [ + { "value": "row", "label": "Horizontal" }, + { "value": "column", "label": "Vertical" } + ], + "default": "column", + "available_if": "{{ block.settings.layout_style == 'flex' }}" + }, + { + "type": "range", + "id": "number_of_columns", + "label": "t:settings.number_of_columns", + "min": 1, + "max": 8, + "step": 1, + "default": 4, + "available_if": "{{ block.settings.layout_style == 'grid' }}" + }, + { + "type": "range", + "id": "gap", + "label": "Gap", + "min": 0, + "max": 100, + "step": 4, + "unit": "px", + "default": 12 + }, + { + "type": "select", + "id": "horizontal_alignment", + "label": "Horizontal alignment", + "options": [ + { "value": "flex-start", "label": "Start" }, + { "value": "center", "label": "Center" }, + { "value": "flex-end", "label": "End" } + ], + "default": "flex-start", + "available_if": "{{ block.settings.content_direction == 'row' }}" + }, + { + "type": "select", + "id": "vertical_alignment", + "label": "Vertical alignment", + "options": [ + { "value": "flex-start", "label": "Start" }, + { "value": "center", "label": "Center" }, + { "value": "flex-end", "label": "End" } + ], + "default": "center", + "available_if": "{{ block.settings.content_direction == 'row' }}" + }, + { + "type": "select", + "id": "horizontal_alignment_flex_direction_column", + "label": "Horizontal alignment", + "options": [ + { "value": "flex-start", "label": "Start" }, + { "value": "center", "label": "Center" }, + { "value": "flex-end", "label": "End" } + ], + "default": "flex-start", + "available_if": "{{ block.settings.content_direction == 'column' }}" + }, + { + "type": "select", + "id": "vertical_alignment_flex_direction_column", + "label": "Vertical alignment", + "options": [ + { "value": "flex-start", "label": "Start" }, + { "value": "center", "label": "Center" }, + { "value": "flex-end", "label": "End" } + ], + "default": "center", + "available_if": "{{ block.settings.content_direction == 'column' }}" + }, + { + "type": "select", + "id": "width", + "label": "Width", + "options": [ + { + "value": "fit-content", + "label": "t:options.fit_content" + }, + { + "value": "fill", + "label": "Fill" + }, + { + "value": "custom", + "label": "t:options.custom" + } + ], + "default": "fill" + }, + { + "type": "range", + "id": "custom_width", + "label": "t:settings.width", + "min": 0, + "max": 100, + "step": 1, + "unit": "%", + "default": 100, + "available_if": "{{ block.settings.width == 'custom' }}" + }, + { + "type": "checkbox", + "id": "enable_sticky_content", + "label": "t:settings.enable_sticky_content", + "default": false + }, + { + "type": "header", + "content": "t:content.colors" + }, + { + "type": "checkbox", + "id": "inherit_color_scheme", + "label": "t:settings.inherit_color_scheme", + "default": true + }, + { + "type": "color_scheme", + "id": "color_scheme", + "label": "t:settings.color_scheme", + "default": "scheme-1", + "available_if": "{{ block.settings.inherit_color_scheme == false }}" + }, + { + "type": "header", + "content": "t:content.background" + }, + { + "type": "video", + "id": "video", + "label": "t:settings.video" + }, + { + "type": "checkbox", + "id": "video_loop", + "label": "t:settings.video_loop", + "default": true, + "available_if": "{{ block.settings.video }}" + }, + { + "type": "select", + "id": "video_position", + "label": "t:settings.video_position", + "options": [ + { + "value": "cover", + "label": "t:options.cover" + }, + { + "value": "contain", + "label": "t:options.contain" + } + ], + "default": "cover", + "available_if": "{{ block.settings.video }}" + }, + { + "type": "range", + "id": "background_video_opacity", + "min": 0, + "max": 100, + "step": 1, + "unit": "%", + "label": "t:settings.overlay_opacity", + "default": 100, + "available_if": "{{ block.settings.video }}" + }, + { + "type": "image_picker", + "id": "background_image", + "label": "t:settings.image" + }, + { + "type": "select", + "id": "background_image_position", + "label": "t:settings.image_position", + "options": [ + { + "value": "cover", + "label": "t:options.cover" + }, + { + "value": "fit", + "label": "t:options.fit" + } + ], + "default": "cover", + "available_if": "{{ block.settings.background_image }}" + }, + { + "type": "range", + "id": "background_image_opacity", + "min": 0, + "max": 100, + "step": 1, + "unit": "%", + "label": "t:settings.image_opacity", + "default": 100, + "available_if": "{{ block.settings.background_image }}" + }, + { + "type": "header", + "content": "t:content.borders" + }, + { + "type": "select", + "id": "border", + "label": "t:settings.borders", + "options": [ + { + "value": "none", + "label": "t:options.none" + }, + { + "value": "solid", + "label": "t:options.solid" + }, + { + "value": "dashed", + "label": "t:options.dashed" + } + ], + "default": "none" + }, + { + "type": "range", + "id": "border_width", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "label": "t:settings.width", + "default": 1, + "available_if": "{{ block.settings.border != 'none' }}" + }, + { + "type": "range", + "id": "border_opacity", + "min": 0, + "max": 100, + "step": 1, + "unit": "%", + "label": "t:settings.opacity", + "default": 100, + "available_if": "{{ block.settings.border != 'none' }}" + }, + { + "type": "range", + "id": "border_radius", + "label": "t:settings.border_radius", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 0 + }, + { + "type": "header", + "content": "Padding" + }, + { + "type": "range", + "id": "padding-block-start", + "label": "Top", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 0 + }, + { + "type": "range", + "id": "padding-block-end", + "label": "Bottom", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 0 + }, + { + "type": "range", + "id": "padding-inline-start", + "label": "Left", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 0 + }, + { + "type": "range", + "id": "padding-inline-end", + "label": "Right", + "min": 0, + "max": 100, + "step": 1, + "unit": "px", + "default": 0 + } + ], + "presets": [ + { + "name": "Group" + } + ] +} +{% endschema %} + `, + 'blocks/slide.liquid': ` + {% schema %} + { + "name": "Slide", + "settings": [ + { + "type": "text", + "id": "text", + "label": "Text" + } + ] + } + {% endschema %} + `, + 'sections/slideshow.liquid': ` + {% schema %} + { + "name": "t:names.slideshow", + "blocks": [ + { + "type": "slide" + } + ], + "settings": [ + { + "type": "select", + "id": "slide_height", + "label": "t:settings.slide_height", + "default": "medium", + "options": [ + { "value": "adapt_image", "label": "t:options.adapt_to_image" }, + { "value": "small", "label": "t:options.small" }, + { "value": "medium", "label": "t:options.medium" }, + { "value": "large", "label": "t:options.large" } + ] + }, + { + "type": "select", + "id": "transition_style", + "label": "t:settings.transition", + "default": "horizontal", + "options": [ + { "value": "horizontal", "label": "t:options.horizontal" }, + { "value": "vertical", "label": "t:options.vertical" } + ] + } + ], + "presets": [ + { + "name": "t:names.slideshow", + "blocks": [ + { + "type": "slide", + "blocks": [ + { + "type": "group", + "settings": { + "layout_style": "flex", + "width": "custom", + "custom_width": 50, + "content_direction": "column", + "padding-inline-start": 48, + "padding-inline-end": 48, + "padding-block-start": 48, + "padding-block-end": 48, + "vertical_alignment_flex_direction_column": "flex-start", + "background_image_position": "cover", + "background_image_opacity": 100, + "border": "none", + "border_width": 1, + "border_opacity": 100 + }, + "blocks": [ + { + "type": "text", + "settings": { + "text": "

Heading

" + } + }, + { + "type": "text" + }, + { + "type": "button" + } + ] + } + ] + }, + { + "type": "slide", + "blocks": [ + { + "type": "group", + "settings": { + "layout_style": "flex", + "width": "custom", + "custom_width": 50, + "content_direction": "column", + "padding-inline-start": 48, + "padding-inline-end": 48, + "padding-block-start": 48, + "padding-block-end": 48, + "vertical_alignment_flex_direction_column": "flex-start", + "background_image_position": "cover", + "background_image_opacity": 100, + "border": "none", + "border_width": 1, + "border_opacity": 100, + "undefined_setting": "list" + }, + "blocks": [ + { + "type": "text", + "settings": { + "text": "

Heading

" + } + }, + { + "type": "text" + }, + { + "type": "button" + } + ] + } + ] + } + ] + } + ] + } + {% endschema %} + `, + }; + const offenses = await check(theme, [ValidPresetSettings]); + expect(offenses).to.have.length(0); + }); +}); diff --git a/packages/theme-check-common/src/checks/valid-preset-settings/index.ts b/packages/theme-check-common/src/checks/valid-preset-settings/index.ts new file mode 100644 index 000000000..b7485d002 --- /dev/null +++ b/packages/theme-check-common/src/checks/valid-preset-settings/index.ts @@ -0,0 +1,151 @@ +import { isSection, isBlock } from '../../to-schema'; +import { basename } from '../../path'; +import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types'; +import { nodeAtPath } from '../../json'; +import { ObjectNode, PropertyNode } from 'json-to-ast'; +import { getBlocks } from '../valid-block-target/block-utils'; + +export const ValidPresetSettings: LiquidCheckDefinition = { + meta: { + code: 'ValidPresetSettings', + name: 'Reports invalid preset settings for sections and blocks', + docs: { + description: 'Reports invalid preset settings for sections and blocks', + recommended: true, + url: 'https://shopify.dev/docs/storefronts/themes/tools/theme-check/checks/valid-preset-settings', + }, + severity: Severity.ERROR, + type: SourceCodeType.LiquidHtml, + schema: {}, + targets: [], + }, + + create(context) { + function getSchema() { + const name = basename(context.file.uri, '.liquid'); + switch (true) { + case isBlock(context.file.uri): + return context.getBlockSchema?.(name); + case isSection(context.file.uri): + return context.getSectionSchema?.(name); + default: + return undefined; + } + } + + const getPresetSettingIds = (presetNode: ObjectNode) => { + return presetNode.children + .map((preset: PropertyNode) => { + const settingsNode = preset.children.find( + (prop: PropertyNode) => prop.key.value === 'settings', + ); + if (settingsNode?.value?.children) { + return settingsNode.value.children.map((setting: PropertyNode) => { + const key = setting.key.value; + const start = setting.loc?.start; + const end = setting.loc?.end; + return { key, start, end }; + }); + } + return []; + }) + .flat() + .filter(Boolean); + }; + + return { + async LiquidRawTag() { + const schema = await getSchema(); + if (!schema) return; + const { validSchema, ast } = schema ?? {}; + if (!validSchema || validSchema instanceof Error) return; + if (!ast || ast instanceof Error) return; + + const presetNode = nodeAtPath(ast, ['presets']) as ObjectNode; + if (!presetNode) return; + + const settingsNode = nodeAtPath(ast, ['settings']) as ObjectNode; + if (!settingsNode) return; + + const presetSettingsIds = getPresetSettingIds(presetNode); + + const settingIds = settingsNode.children.map((child: PropertyNode) => { + const idNode = child.children?.find((prop: PropertyNode) => prop.key.value === 'id'); + return idNode?.value?.value; + }); + + for (const presetSettingId of presetSettingsIds) { + if (!settingIds.includes(presetSettingId.key)) { + context.report({ + startIndex: presetSettingId.start.offset, + endIndex: presetSettingId.end.offset, + message: `Preset setting "${presetSettingId.key}" does not exist in settings`, + }); + } + } + + const { rootLevelThemeBlocks, rootLevelLocalBlocks, presetLevelBlocks } = + getBlocks(validSchema); + + const rootLevelBlockSettingIds = await Promise.all( + [...rootLevelThemeBlocks, ...rootLevelLocalBlocks].flat().map(async ({ node }) => { + const blockSchema = await context.getBlockSchema?.(node.type); + const { validSchema, ast } = blockSchema ?? {}; + + if (!validSchema || validSchema instanceof Error) return []; + if (!ast || ast instanceof Error) return []; + + const settingsNode = nodeAtPath(ast, ['settings']) as ObjectNode; + if (!settingsNode?.children) return []; + return settingsNode.children + .filter((settingObj: PropertyNode) => { + const typeNode = settingObj.children?.find( + (prop: PropertyNode) => prop.key.value === 'type', + ); + return typeNode?.value?.value !== 'header'; + }) + .map((settingObj: PropertyNode) => { + const idNode = settingObj.children.find( + (prop: PropertyNode) => prop.key.value === 'id', + ); + return idNode?.value?.value; + }) + .filter((id): id is string => Boolean(id)); + }), + ); + + let presetBlockSettingIds: { key: string; start: any; end: any }[] = []; + await Promise.all( + Object.values(presetLevelBlocks) + .flat() + .map(async (block) => { + const blockPath = block.path.slice(0, -1); + const blockNode = nodeAtPath(ast, blockPath) as ObjectNode; + if (!blockNode) return; + + const settings = Object.values(block.node)[0]?.settings; + if (settings) { + for (const [key, value] of Object.entries(settings)) { + presetBlockSettingIds.push({ + key, + start: blockNode.loc?.start, + end: blockNode.loc?.end, + }); + } + } + }), + ); + + for (const presetBlockSettingId of presetBlockSettingIds) { + if (!rootLevelBlockSettingIds.flat().some((id) => id === presetBlockSettingId.key)) { + context.report({ + startIndex: presetBlockSettingId?.start?.line ?? 0, + endIndex: presetBlockSettingId?.end?.line ?? 0, + message: `Preset block setting "${presetBlockSettingId.key}" does not exist in settings.`, + }); + } + } + }, + }; + }, +}; diff --git a/packages/theme-check-node/configs/all.yml b/packages/theme-check-node/configs/all.yml index e9f80d36b..152a5eec6 100644 --- a/packages/theme-check-node/configs/all.yml +++ b/packages/theme-check-node/configs/all.yml @@ -118,7 +118,7 @@ UnknownFilter: UnusedAssign: enabled: true severity: 1 -ValidBlockPresetSettings: +ValidPresetSettings: enabled: true severity: 0 ValidBlockTarget: diff --git a/packages/theme-check-node/configs/recommended.yml b/packages/theme-check-node/configs/recommended.yml index 67029273f..2a9dba1f9 100644 --- a/packages/theme-check-node/configs/recommended.yml +++ b/packages/theme-check-node/configs/recommended.yml @@ -99,7 +99,7 @@ UnusedAssign: ValidBlockTarget: enabled: true severity: 0 -ValidBlockPresetSettings: +ValidPresetSettings: enabled: true severity: 0 ValidContentForArguments: