From bb8777512f43064d96f0a2c327d35c3fca470182 Mon Sep 17 00:00:00 2001 From: Ariba Rajput Date: Wed, 27 Nov 2024 16:29:30 +0000 Subject: [PATCH 1/4] Added in a check for theme block settings validation --- .../checks/presets-key-matches/index.spec.ts | 0 .../src/checks/presets-key-matches/index.ts | 25 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 packages/theme-check-common/src/checks/presets-key-matches/index.spec.ts create mode 100644 packages/theme-check-common/src/checks/presets-key-matches/index.ts diff --git a/packages/theme-check-common/src/checks/presets-key-matches/index.spec.ts b/packages/theme-check-common/src/checks/presets-key-matches/index.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/theme-check-common/src/checks/presets-key-matches/index.ts b/packages/theme-check-common/src/checks/presets-key-matches/index.ts new file mode 100644 index 00000000..57d27ce4 --- /dev/null +++ b/packages/theme-check-common/src/checks/presets-key-matches/index.ts @@ -0,0 +1,25 @@ +import { AugmentedDependencies, CheckNodeMethod, LiquidCheckDefinition, LiquidHtmlNode, LiquidHtmlNodeTypes, RelativePath, Schema, Settings, Severity, SourceCodeType, StringCorrector, UriString, ValidateJSON } from "../.."; + +export const PresetsKeyMatches: LiquidCheckDefinition = { + meta: { + name: "PresetsKeyMatches", + code: "PresetsKeyMatches", + severity: Severity.ERROR, + type: SourceCodeType.LiquidHtml, + docs: { + description: "", + recommended: undefined, + url: undefined + }, + schema: {}, + targets: [], + }, + + create(context){ + return{ + async Document(node){ + debugger; + } + } + } +} From 625d79ee2660c103e776db61cbafa393cc28652d Mon Sep 17 00:00:00 2001 From: Ariba Rajput Date: Thu, 28 Nov 2024 14:31:01 -0500 Subject: [PATCH 2/4] theme block theme check Finished tests Added changeset --- .changeset/chilled-bugs-juggle.md | 6 + .../theme-check-common/src/checks/index.ts | 2 + .../checks/presets-key-matches/index.spec.ts | 0 .../src/checks/presets-key-matches/index.ts | 25 --- .../valid-block-preset-settings/index.spec.ts | 166 ++++++++++++++++++ .../valid-block-preset-settings/index.ts | 116 ++++++++++++ packages/theme-check-node/configs/all.yml | 3 + .../theme-check-node/configs/recommended.yml | 3 + 8 files changed, 296 insertions(+), 25 deletions(-) create mode 100644 .changeset/chilled-bugs-juggle.md delete mode 100644 packages/theme-check-common/src/checks/presets-key-matches/index.spec.ts delete mode 100644 packages/theme-check-common/src/checks/presets-key-matches/index.ts create mode 100644 packages/theme-check-common/src/checks/valid-block-preset-settings/index.spec.ts create mode 100644 packages/theme-check-common/src/checks/valid-block-preset-settings/index.ts diff --git a/.changeset/chilled-bugs-juggle.md b/.changeset/chilled-bugs-juggle.md new file mode 100644 index 00000000..4373b37e --- /dev/null +++ b/.changeset/chilled-bugs-juggle.md @@ -0,0 +1,6 @@ +--- +'@shopify/theme-check-common': minor +'theme-check-vscode': minor +--- + +Add the `ValidBlockPresetSettings` check. diff --git a/packages/theme-check-common/src/checks/index.ts b/packages/theme-check-common/src/checks/index.ts index 88d5802d..e0803940 100644 --- a/packages/theme-check-common/src/checks/index.ts +++ b/packages/theme-check-common/src/checks/index.ts @@ -45,6 +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'; export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [ AppBlockValidTags, @@ -92,6 +93,7 @@ export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [ ValidStaticBlockType, VariableName, ValidSchemaName, + ValidBlockPresetSettings, ]; /** diff --git a/packages/theme-check-common/src/checks/presets-key-matches/index.spec.ts b/packages/theme-check-common/src/checks/presets-key-matches/index.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/theme-check-common/src/checks/presets-key-matches/index.ts b/packages/theme-check-common/src/checks/presets-key-matches/index.ts deleted file mode 100644 index 57d27ce4..00000000 --- a/packages/theme-check-common/src/checks/presets-key-matches/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { AugmentedDependencies, CheckNodeMethod, LiquidCheckDefinition, LiquidHtmlNode, LiquidHtmlNodeTypes, RelativePath, Schema, Settings, Severity, SourceCodeType, StringCorrector, UriString, ValidateJSON } from "../.."; - -export const PresetsKeyMatches: LiquidCheckDefinition = { - meta: { - name: "PresetsKeyMatches", - code: "PresetsKeyMatches", - severity: Severity.ERROR, - type: SourceCodeType.LiquidHtml, - docs: { - description: "", - recommended: undefined, - url: undefined - }, - schema: {}, - targets: [], - }, - - create(context){ - return{ - async Document(node){ - debugger; - } - } - } -} 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 new file mode 100644 index 00000000..944dee03 --- /dev/null +++ b/packages/theme-check-common/src/checks/valid-block-preset-settings/index.spec.ts @@ -0,0 +1,166 @@ +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 new file mode 100644 index 00000000..faafba79 --- /dev/null +++ b/packages/theme-check-common/src/checks/valid-block-preset-settings/index.ts @@ -0,0 +1,116 @@ +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-node/configs/all.yml b/packages/theme-check-node/configs/all.yml index 62227ec2..e9f80d36 100644 --- a/packages/theme-check-node/configs/all.yml +++ b/packages/theme-check-node/configs/all.yml @@ -118,6 +118,9 @@ UnknownFilter: UnusedAssign: enabled: true severity: 1 +ValidBlockPresetSettings: + enabled: true + severity: 0 ValidBlockTarget: enabled: true severity: 0 diff --git a/packages/theme-check-node/configs/recommended.yml b/packages/theme-check-node/configs/recommended.yml index 7640452a..67029273 100644 --- a/packages/theme-check-node/configs/recommended.yml +++ b/packages/theme-check-node/configs/recommended.yml @@ -99,6 +99,9 @@ UnusedAssign: ValidBlockTarget: enabled: true severity: 0 +ValidBlockPresetSettings: + enabled: true + severity: 0 ValidContentForArguments: enabled: true severity: 0 From 6f90bcc66f815a2c8e22a6069729aee8627b4caa Mon Sep 17 00:00:00 2001 From: Ariba Rajput Date: Thu, 5 Dec 2024 16:20:30 -0500 Subject: [PATCH 3/4] 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 4373b37e..379aede8 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 e0803940..1bc31d89 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 944dee03..00000000 --- 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 faafba79..00000000 --- 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 00000000..bddd0c2f --- /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 00000000..b7485d00 --- /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 e9f80d36..152a5eec 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 67029273..2a9dba1f 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: From 8745150258dbd4540649f6078d737aceb7ea66af Mon Sep 17 00:00:00 2001 From: Ariba Rajput Date: Thu, 19 Dec 2024 17:40:34 -0500 Subject: [PATCH 4/4] Added in default setting validation --- .changeset/chilled-bugs-juggle.md | 2 +- .../theme-check-common/src/checks/index.ts | 4 +- .../index.spec.ts | 928 ++++++++---------- .../index.ts | 219 +++++ .../src/checks/valid-preset-settings/index.ts | 151 --- .../theme-check-common/src/jsonc/types.ts | 1 + packages/theme-check-node/configs/all.yml | 2 +- .../theme-check-node/configs/recommended.yml | 2 +- 8 files changed, 643 insertions(+), 666 deletions(-) rename packages/theme-check-common/src/checks/{valid-preset-settings => valid-preset-and-default-settings}/index.spec.ts (54%) create mode 100644 packages/theme-check-common/src/checks/valid-preset-and-default-settings/index.ts delete 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 379aede8..8862a16e 100644 --- a/.changeset/chilled-bugs-juggle.md +++ b/.changeset/chilled-bugs-juggle.md @@ -3,4 +3,4 @@ 'theme-check-vscode': minor --- -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. +Add the `ValidPresetAndDefaultSettings` 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 1bc31d89..99e12f2c 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 { ValidPresetSettings } from './valid-preset-settings'; +import { ValidPresetAndDefaultSettings } from './valid-preset-and-default-settings'; export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [ AppBlockValidTags, @@ -93,7 +93,7 @@ export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [ ValidStaticBlockType, VariableName, ValidSchemaName, - ValidPresetSettings, + ValidPresetAndDefaultSettings, ]; /** diff --git a/packages/theme-check-common/src/checks/valid-preset-settings/index.spec.ts b/packages/theme-check-common/src/checks/valid-preset-and-default-settings/index.spec.ts similarity index 54% rename from packages/theme-check-common/src/checks/valid-preset-settings/index.spec.ts rename to packages/theme-check-common/src/checks/valid-preset-and-default-settings/index.spec.ts index bddd0c2f..ca80186c 100644 --- a/packages/theme-check-common/src/checks/valid-preset-settings/index.spec.ts +++ b/packages/theme-check-common/src/checks/valid-preset-and-default-settings/index.spec.ts @@ -1,15 +1,15 @@ import { describe, expect, it } from 'vitest'; -import { ValidPresetSettings } from '.'; import { check } from '../../test/test-helper'; import { MockTheme } from '../../test/MockTheme'; +import { ValidPresetAndDefaultSettings } from '.'; -describe('ValidPresetSettings', () => { - it('should report invalid keys in a blocks preset setting', async () => { +describe('ValidPresetAndDefaultSettings', () => { + it('should report invalid keys in a blocks preset settings', async () => { const theme: MockTheme = { - 'blocks/price.liquid': ` + 'blocks/test_block.liquid': ` {% schema %} { - "name": "t:names.product_price", + "name": "t:names.test_block", "settings": [ { "type": "product", @@ -24,10 +24,10 @@ describe('ValidPresetSettings', () => { ], "presets": [ { - "name": "t:names.product_price", - "settings": { + "name": "t:names.test_block", + "settings": { "product": "{{ context.product }}", - "undefined_setting": "some value" + "undefined_setting": "this is an invalid setting as this key does not exist" } } ] @@ -36,33 +36,68 @@ describe('ValidPresetSettings', () => { `, }; - const offenses = await check(theme, [ValidPresetSettings]); + const offenses = await check(theme, [ValidPresetAndDefaultSettings]); 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 () => { + it('should report invalid keys in a blocks default settings', async () => { + const theme: MockTheme = { + 'blocks/test_block.liquid': ` + {% schema %} + { + "name": "t:names.product_price", + "settings": [ + { + "type": "product", + "id": "product", + "label": "t:settings.product" + }, + { + "type": "collection", + "id": "collection", + "label": "t:settings.collection" + } + ], + "default": { + "settings": { + "undefined_setting": "incorrect setting key" + } + } + } + {% endschema %} + `, + }; + + const offenses = await check(theme, [ValidPresetAndDefaultSettings]); + expect(offenses).to.have.length(1); + expect(offenses[0].message).to.include( + 'Default setting "undefined_setting" does not exist in settings', + ); + }); + + it('should report invalid keys in a blocks nested 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" - } - ] + "settings": [ + { + "type": "text", + "id": "block_1_setting_key", + "label": "t:settings.block_1" + } + ] } {% endschema %} `, - 'blocks/price.liquid': ` + 'blocks/test_block.liquid': ` {% schema %} { - "name": "t:names.product_price", + "name": "t:names.test_block", "settings": [ { "type": "product", @@ -83,7 +118,7 @@ describe('ValidPresetSettings', () => { ], "presets": [ { - "name": "t:names.product_price", + "name": "Preset 1", "settings": { "product": "{{ context.product }}", "collection": "{{ context.collection }}" @@ -99,13 +134,6 @@ describe('ValidPresetSettings', () => { } } ] - }, - { - "name": "t:names.product_price_2", - "settings": { - "product": "{{ context.product }}", - "collection": "{{ context.collection }}" - } } ] } @@ -113,14 +141,35 @@ describe('ValidPresetSettings', () => { `, }; - const offenses = await check(theme, [ValidPresetSettings]); + // TODO: fix this test + /** + * "block_1": { + "type": "block_1", + "settings": { + "block_1_setting_key": "correct setting key", + "undefined_setting": "incorrect setting key" + } + } + } + * + * or + * + * { + * "type": "block_1", + * "settings": { + * "block_1_setting_key": "correct setting key", + * "undefined_setting": "incorrect setting key" + * } + * } + */ + const offenses = await check(theme, [ValidPresetAndDefaultSettings]); 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 () => { + it('should report invalid keys in a blocks nested block default setting', async () => { const theme: MockTheme = { 'blocks/block_1.liquid': ` {% schema %} @@ -133,9 +182,9 @@ describe('ValidPresetSettings', () => { "label": "t:settings.block_1" } ] - } - {% endschema %} - `, + } + {% endschema %} + `, 'blocks/price.liquid': ` {% schema %} { @@ -147,9 +196,9 @@ describe('ValidPresetSettings', () => { "label": "t:settings.product" }, { - "type": "text", - "id": "section_setting", - "label": "t:settings.section" + "type": "collection", + "id": "collection", + "label": "t:settings.collection" } ], "blocks": [ @@ -158,31 +207,107 @@ describe('ValidPresetSettings', () => { "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" - } + "default": { + "settings": { + "product": "{{ context.product }}", + "collection": "{{ context.collection }}" }, + "blocks": [ + { + "type": "block_1", + "settings": { + "block_1_setting_key": "correct setting key", + "undefined_setting": "incorrect setting key" } + } + ] + } + } + {% endschema %} + `, + }; + + const offenses = await check(theme, [ValidPresetAndDefaultSettings]); + expect(offenses).to.have.length(1); + expect(offenses[0].message).to.include( + `Default block setting "undefined_setting" does not exist in settings`, + ); + }); + + it('should not report when all preset and default 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/test_block.liquid': ` + {% schema %} + { + "name": "t:names.test_block", + "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": [ + { + "type": "block_1", + "settings": { + "block_1_setting_key": "correct setting key", } - ] + } + ] + } + ], + "default": { + "settings": { + "product": "{{ context.product }}", + "section_setting": "some value" + }, + "blocks": [ + { + "type": "block_1", + "settings": { + "block_1_setting_key": "correct setting key", + } } ] } - {% endschema %} - `, + } + {% endschema %} + `, }; - const offenses = await check(theme, [ValidPresetSettings]); + const offenses = await check(theme, [ValidPresetAndDefaultSettings]); expect(offenses).to.have.length(0); }); @@ -379,29 +504,15 @@ describe('ValidPresetSettings', () => { `, }; - const offenses = await check(theme, [ValidPresetSettings]); + const offenses = await check(theme, [ValidPresetAndDefaultSettings]); 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 () => { + it('should report invalid keys in a sections default setting', async () => { const theme: MockTheme = { - 'blocks/announcement.liquid': ` - {% schema %} - { - "name": "Announcement", - "settings": [ - { - "type": "text", - "id": "text", - "label": "Text" - } - ] - } - {% endschema %} - `, 'sections/header-announcements.liquid': ` {% schema %} { @@ -580,6 +691,92 @@ describe('ValidPresetSettings', () => { "default": 0 } ], + "default": { + "settings": { + "undefined_setting": "list" + } + } + } + {% endschema %} + `, + }; + + const offenses = await check(theme, [ValidPresetAndDefaultSettings]); + expect(offenses).to.have.length(1); + expect(offenses[0].message).to.include( + `Default 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": "range", + "id": "gap", + "label": "Gap", + "min": 0, + "max": 100, + "unit": "px", + "default": 16, + "available_if": "{{ section.settings.show_as == 'list' }}" + } + ], "presets": [ { "name": "Announcement bar", @@ -588,7 +785,7 @@ describe('ValidPresetSettings', () => { "announcement": { "type": "announcement", "settings": { - "undefined_setting": "list" + "undefined_setting": "this is an invalid setting" } } } @@ -600,365 +797,112 @@ describe('ValidPresetSettings', () => { `, }; - const offenses = await check(theme, [ValidPresetSettings]); + const offenses = await check(theme, [ValidPresetAndDefaultSettings]); 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 () => { + it('should report invalid keys in a sections nested block default setting', async () => { const theme: MockTheme = { - 'blocks/group.liquid': ` + 'blocks/announcement.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" + "name": "Announcement", + "settings": [ + { + "type": "text", + "id": "text", + "label": "Text" + } + ] } - ], - "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" - }, + {% endschema %} + `, + 'sections/header-announcements.liquid': ` + {% schema %} { - "value": "dashed", - "label": "t:options.dashed" + "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": "range", + "id": "gap", + "label": "Gap", + "min": 0, + "max": 100, + "unit": "px", + "default": 16, + "available_if": "{{ section.settings.show_as == 'list' }}" + } + ], + "default": { + "settings": { + "gap": 20 + }, + "blocks": [ + { + "type": "announcement", + "settings": { + "undefined_setting": "this is an invalid setting" + } + } + ] + } } - ], - "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 %} + {% endschema %} `, - 'blocks/slide.liquid': ` + }; + + const offenses = await check(theme, [ValidPresetAndDefaultSettings]); + expect(offenses[0].message).to.include( + `Default block setting "undefined_setting" does not exist in settings`, + ); + }); + + it('should not report when all preset and default settings in the section are valid', async () => { + const theme: MockTheme = { + 'blocks/announcement.liquid': ` {% schema %} { - "name": "Slide", + "name": "Announcement", "settings": [ { "type": "text", @@ -969,128 +913,92 @@ describe('ValidPresetSettings', () => { } {% endschema %} `, - 'sections/slideshow.liquid': ` + 'sections/header-announcements.liquid': ` {% schema %} { - "name": "t:names.slideshow", + "name": "Announcement bar", + "tag": "aside", "blocks": [ { - "type": "slide" + "type": "announcement" } ], + "enabled_on": { + "groups": [ + "header" + ] + }, "settings": [ { "type": "select", - "id": "slide_height", - "label": "t:settings.slide_height", - "default": "medium", + "id": "show_as", + "label": "Type", "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" } + { + "value": "carousel", + "label": "Carousel" + }, + { + "value": "list", + "label": "List" + }, + { + "value": "scroll", + "label": "Scroll" + } ] }, { - "type": "select", - "id": "transition_style", - "label": "t:settings.transition", - "default": "horizontal", - "options": [ - { "value": "horizontal", "label": "t:options.horizontal" }, - { "value": "vertical", "label": "t:options.vertical" } - ] + "type": "checkbox", + "id": "auto_rotate", + "label": "Auto rotate", + "default": true, + "available_if": "{{ section.settings.show_as == 'carousel' }}" + }, + { + "type": "range", + "id": "gap", + "label": "Gap", + "min": 0, + "max": 100, + "unit": "px", + "default": 16, + "available_if": "{{ section.settings.show_as == 'list' }}" } ], - "presets": [ + "presets": [ { - "name": "t:names.slideshow", + "name": "Announcement bar", "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" - } - ] + "announcement": { + "type": "announcement", + "settings": { + "text": "some text" } - ] + } } ] } - ] + ], + "default": { + "settings": { + "gap": 20 + }, + "blocks": [ + { + "type": "announcement", + "settings": { + "text": "some text + } + } + ] + } } {% endschema %} `, }; - const offenses = await check(theme, [ValidPresetSettings]); + const offenses = await check(theme, [ValidPresetAndDefaultSettings]); expect(offenses).to.have.length(0); }); }); diff --git a/packages/theme-check-common/src/checks/valid-preset-and-default-settings/index.ts b/packages/theme-check-common/src/checks/valid-preset-and-default-settings/index.ts new file mode 100644 index 00000000..432dca74 --- /dev/null +++ b/packages/theme-check-common/src/checks/valid-preset-and-default-settings/index.ts @@ -0,0 +1,219 @@ +import { isSection, isBlock } from '../../to-schema'; +import { basename } from '../../path'; +import { + LiquidCheckDefinition, + ObjectNode, + PropertyNode, + Severity, + SourceCodeType, +} from '../../types'; +import { nodeAtPath } from '../../json'; +import { getBlocks } from '../valid-block-target/block-utils'; + +export const ValidPresetAndDefaultSettings: LiquidCheckDefinition = { + meta: { + code: 'ValidPresetAndDefaultSettings', + name: 'Reports invalid preset and default settings for sections and blocks', + docs: { + description: 'Reports invalid preset and default settings for sections and blocks', + recommended: true, + url: 'https://shopify.dev/docs/storefronts/themes/tools/theme-check/checks/valid-preset-and-default-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 getNestedSettingIds = (node: ObjectNode) => { + return node.children + .map((property: PropertyNode) => { + const settingsNode = property.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 settingsNode = nodeAtPath(ast, ['settings']) as ObjectNode; + const presetNode = nodeAtPath(ast, ['presets']) as ObjectNode; + const defaultNode = nodeAtPath(ast, ['default']) as ObjectNode; + + const settingIds = + settingsNode?.children?.map((child: PropertyNode) => { + const idNode = child.children?.find((prop: PropertyNode) => prop.key.value === 'id'); + return idNode?.value && 'value' in idNode.value ? idNode.value.value : undefined; + }) ?? []; + + const defaultSettingsIds = + defaultNode?.children + ?.find((child: PropertyNode) => child.key.value === 'settings') + ?.value?.children?.map((child: PropertyNode) => ({ + key: child.key.value, + start: child.loc!.start, + end: child.loc!.end, + })) ?? []; + const presetSettingsIds = presetNode ? getNestedSettingIds(presetNode) : []; + + 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`, + }); + } + } + + for (const defaultSettingId of defaultSettingsIds) { + if (!settingIds.includes(defaultSettingId.key)) { + context.report({ + startIndex: defaultSettingId!.start.offset, + endIndex: defaultSettingId!.end.offset, + message: `Default setting "${defaultSettingId.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 + .map((settingObj: PropertyNode) => { + const idNode = settingObj.children.find( + (prop: { key: { value: string } }) => prop.key.value === 'id', + ); + return idNode?.value?.value; + }) + .filter((id: string): 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; + + let settings; + if ('settings' in block.node) { + settings = block.node.settings; + } else { + const blockTypeKey = Object.keys(block.node)[0]; + settings = block.node[blockTypeKey]?.settings; + } + + if (settings) { + for (const [key, value] of Object.entries(settings)) { + presetBlockSettingIds.push({ + key, + start: blockNode.loc?.start, + end: blockNode.loc?.end, + }); + } + } + }), + ); + const defaultBlockSettingIds: { key: string; start: any; end: any }[] = []; + const defaultBlocks = defaultNode?.children?.find( + (child: PropertyNode) => child.key.value === 'blocks', + )?.value; + + const extractBlockSettings = (block: PropertyNode) => { + let settingsNode; + + if (block.value && 'children' in block.value) { + settingsNode = block.value.children?.find( + (child: PropertyNode) => child.key.value === 'settings', + ); + } else { + settingsNode = block.children?.find( + (child: PropertyNode) => child.key.value === 'settings', + ); + } + + return settingsNode; + }; + + if (defaultBlocks && 'children' in defaultBlocks) { + defaultBlocks.children.forEach((block: PropertyNode) => { + const settingsNode = extractBlockSettings(block); + + if (settingsNode?.value && 'children' in settingsNode.value) { + settingsNode.value.children.forEach((setting: PropertyNode) => { + defaultBlockSettingIds.push({ + key: setting.key.value, + start: setting.loc?.start, + end: setting.loc?.end, + }); + }); + } + }); + } + + for (const defaultBlockSettingId of defaultBlockSettingIds) { + if (!rootLevelBlockSettingIds.flat().some((id) => id === defaultBlockSettingId.key)) { + context.report({ + startIndex: defaultBlockSettingId.start.offset, + endIndex: defaultBlockSettingId.end.offset, + message: `Default block setting "${defaultBlockSettingId.key}" does not exist in settings.`, + }); + } + } + + for (const presetBlockSettingId of presetBlockSettingIds) { + if (!rootLevelBlockSettingIds.flat().some((id) => id === presetBlockSettingId.key)) { + context.report({ + startIndex: presetBlockSettingId!.start.offset, + endIndex: presetBlockSettingId!.end.offset, + message: `Preset block setting "${presetBlockSettingId.key}" does not exist in settings.`, + }); + } + } + }, + }; + }, +}; 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 deleted file mode 100644 index b7485d00..00000000 --- a/packages/theme-check-common/src/checks/valid-preset-settings/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -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-common/src/jsonc/types.ts b/packages/theme-check-common/src/jsonc/types.ts index fdf71865..6eef1c8e 100644 --- a/packages/theme-check-common/src/jsonc/types.ts +++ b/packages/theme-check-common/src/jsonc/types.ts @@ -27,6 +27,7 @@ export interface PropertyNode extends ASTNode { type: 'Property'; key: IdentifierNode; value: ValueNode; + children: any[]; } export interface IdentifierNode extends ASTNode { diff --git a/packages/theme-check-node/configs/all.yml b/packages/theme-check-node/configs/all.yml index 152a5eec..df662e10 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 -ValidPresetSettings: +ValidPresetAndDefaultSettings: enabled: true severity: 0 ValidBlockTarget: diff --git a/packages/theme-check-node/configs/recommended.yml b/packages/theme-check-node/configs/recommended.yml index 2a9dba1f..8c9e49eb 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 -ValidPresetSettings: +ValidPresetAndDefaultSettings: enabled: true severity: 0 ValidContentForArguments: