Skip to content

Commit b95fc84

Browse files
committed
theme block theme check
Finished tests Added changeset
1 parent 00fbe01 commit b95fc84

File tree

8 files changed

+297
-25
lines changed

8 files changed

+297
-25
lines changed

.changeset/chilled-bugs-juggle.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/theme-check-common': minor
3+
---
4+
5+
Added theme check for preset settings validation on theme blocks

packages/theme-check-common/src/checks/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { ValidSchemaName } from './valid-schema-name';
4242
import { ValidStaticBlockType } from './valid-static-block-type';
4343
import { VariableName } from './variable-name';
4444
import { MissingSchema } from './missing-schema';
45+
import { ValidBlockPresetSettings } from './valid-block-preset-settings';
4546

4647
export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [
4748
AppBlockValidTags,
@@ -85,6 +86,7 @@ export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [
8586
ValidStaticBlockType,
8687
VariableName,
8788
ValidSchemaName,
89+
ValidBlockPresetSettings,
8890
];
8991

9092
/**

packages/theme-check-common/src/checks/presets-key-matches/index.spec.ts

Whitespace-only changes.

packages/theme-check-common/src/checks/presets-key-matches/index.ts

-25
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { ValidBlockPresetSettings } from '.';
3+
import { check } from '../../test/test-helper';
4+
import { MockTheme } from '../../test/MockTheme';
5+
6+
describe('ValidBlockPresetSettings', () => {
7+
it('should report invalid preset settings', async () => {
8+
const theme: MockTheme = {
9+
'blocks/price.liquid': `
10+
{% schema %}
11+
{
12+
"name": "t:names.product_price",
13+
"settings": [
14+
{
15+
"type": "product",
16+
"id": "product",
17+
"label": "t:settings.product"
18+
},
19+
],
20+
"presets": [
21+
{
22+
"name": "t:names.product_price",
23+
"settings": {
24+
"product": "{{ context.product }}",
25+
"undefined_setting": "some value",
26+
}
27+
}
28+
]
29+
}
30+
{% endschema %}
31+
`,
32+
};
33+
const offenses = await check(theme, [ValidBlockPresetSettings]);
34+
expect(offenses).to.have.length(1);
35+
});
36+
37+
it('should report invalid theme block preset settings', async () => {
38+
const theme: MockTheme = {
39+
'blocks/block_1.liquid': `
40+
{% schema %}
41+
{
42+
"name": "t:names.block_1",
43+
"settings": [
44+
{
45+
"type": "text",
46+
"id": "block_1_setting_key",
47+
"label": "t:settings.block_1"
48+
},
49+
]
50+
}
51+
{% endschema %}
52+
`,
53+
'blocks/price.liquid': `
54+
{% schema %}
55+
{
56+
"name": "t:names.product_price",
57+
"settings": [
58+
{
59+
"type": "product",
60+
"id": "product",
61+
"label": "t:settings.product"
62+
}
63+
],
64+
"blocks": [
65+
{
66+
"type": "block_1",
67+
"name": "t:names.block_1",
68+
}
69+
],
70+
"presets": [
71+
{
72+
"name": "t:names.product_price",
73+
"settings": {
74+
"product": "{{ context.product }}",
75+
},
76+
"blocks": [
77+
{
78+
"block_1": {
79+
"type": "block_1",
80+
"settings": {
81+
"block_1_setting_key": "correct setting key",
82+
"undefined_setting": "incorrect setting key"
83+
}
84+
}
85+
}
86+
],
87+
}
88+
]
89+
}
90+
{% endschema %}
91+
`,
92+
};
93+
94+
const offenses = await check(theme, [ValidBlockPresetSettings]);
95+
expect(offenses).to.have.length(1);
96+
expect(offenses[0].message).to.include(
97+
'Preset setting "undefined_setting" does not exist in the block type "block_1"\'s settings',
98+
);
99+
});
100+
101+
it('should not report when all section and block preset settings are valid', async () => {
102+
const theme: MockTheme = {
103+
'blocks/block_1.liquid': `
104+
{% schema %}
105+
{
106+
"name": "t:names.block_1",
107+
"settings": [
108+
{
109+
"type": "text",
110+
"id": "block_1_setting_key",
111+
"label": "t:settings.block_1"
112+
}
113+
]
114+
}
115+
{% endschema %}
116+
`,
117+
'blocks/price.liquid': `
118+
{% schema %}
119+
{
120+
"name": "t:names.product_price",
121+
"settings": [
122+
{
123+
"type": "product",
124+
"id": "product",
125+
"label": "t:settings.product"
126+
},
127+
{
128+
"type": "text",
129+
"id": "section_setting",
130+
"label": "t:settings.section"
131+
}
132+
],
133+
"blocks": [
134+
{
135+
"type": "block_1",
136+
"name": "t:names.block_1"
137+
}
138+
],
139+
"presets": [
140+
{
141+
"name": "t:names.product_price",
142+
"settings": {
143+
"product": "{{ context.product }}",
144+
"section_setting": "some value"
145+
},
146+
"blocks": [
147+
{
148+
"block_1": {
149+
"type": "block_1",
150+
"settings": {
151+
"block_1_setting_key": "correct setting key"
152+
}
153+
}
154+
}
155+
]
156+
}
157+
]
158+
}
159+
{% endschema %}
160+
`,
161+
};
162+
163+
const offenses = await check(theme, [ValidBlockPresetSettings]);
164+
expect(offenses).to.have.length(0);
165+
});
166+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//we want to check if the preset key exists in the settings
2+
3+
import { isSection } from '../../to-schema';
4+
import { basename } from '../../path';
5+
import { isBlock } from '../../to-schema';
6+
import { LiquidCheckDefinition, Severity, SourceCodeType, ThemeBlock } from '../../types';
7+
import { Preset } from '../../types/schemas/preset';
8+
9+
export const ValidBlockPresetSettings: LiquidCheckDefinition = {
10+
meta: {
11+
code: 'ValidBlockPresetSettings',
12+
name: 'Reports invalid preset settings for a theme block',
13+
docs: {
14+
description: 'Reports invalid preset settings for a theme block',
15+
recommended: true,
16+
url: 'https://shopify.dev/docs/storefronts/themes/tools/theme-check/checks/preset-key-exists',
17+
},
18+
severity: Severity.ERROR,
19+
type: SourceCodeType.LiquidHtml,
20+
schema: {},
21+
targets: [],
22+
},
23+
24+
create(context) {
25+
function getSchema() {
26+
const name = basename(context.file.uri, '.liquid');
27+
switch (true) {
28+
case isBlock(context.file.uri):
29+
return context.getBlockSchema?.(name);
30+
case isSection(context.file.uri):
31+
return context.getSectionSchema?.(name);
32+
default:
33+
return undefined;
34+
}
35+
}
36+
37+
function getInlineSettingsTypesandKeys(settings: ThemeBlock.Schema['settings']) {
38+
if (!settings) return [];
39+
return settings.map((setting: { id: any; type: any }) => ({
40+
id: setting.id,
41+
type: setting.type,
42+
}));
43+
}
44+
45+
function getPresetSettingsKeys(presets: Preset.Preset[]) {
46+
const allKeys: string[] = [];
47+
for (const preset of presets) {
48+
if (preset.settings) {
49+
allKeys.push(...Object.keys(preset.settings));
50+
}
51+
}
52+
return allKeys;
53+
}
54+
55+
function getPresetBlockSettingsKeys(blocks: Preset.PresetBlocks) {
56+
const allKeys: string[] = [];
57+
for (const block of Object.values(blocks)) {
58+
for (const [_, blockData] of Object.entries(block)) {
59+
if (blockData && typeof blockData === 'object' && 'settings' in blockData) {
60+
const settings = blockData.settings;
61+
if (settings && typeof settings === 'object') {
62+
allKeys.push(...Object.keys(settings));
63+
}
64+
}
65+
}
66+
}
67+
return allKeys;
68+
}
69+
70+
return {
71+
async LiquidRawTag(node) {
72+
if (node.name !== 'schema' || node.body.kind !== 'json') {
73+
return;
74+
}
75+
76+
const schema = await getSchema();
77+
if (!schema) return;
78+
if (schema.validSchema instanceof Error) return;
79+
80+
const validSchema = schema.validSchema;
81+
const settingsKeys = getInlineSettingsTypesandKeys(validSchema.settings);
82+
const presetSettingsKeys = getPresetSettingsKeys(validSchema.presets ?? []);
83+
84+
for (const key of presetSettingsKeys) {
85+
if (!settingsKeys.some((setting) => setting.id === key)) {
86+
context.report({
87+
message: `Preset setting "${key}" does not exist in the block's settings`,
88+
startIndex: 0,
89+
endIndex: 0,
90+
});
91+
}
92+
}
93+
94+
if (validSchema.blocks) {
95+
for (const block of validSchema.blocks) {
96+
const blockSchema = await context.getBlockSchema?.(block.type);
97+
if (!blockSchema || blockSchema.validSchema instanceof Error) continue;
98+
99+
for (const preset of validSchema.presets ?? []) {
100+
if (!preset.blocks) continue;
101+
const presetBlockSettingKeys = getPresetBlockSettingsKeys(preset.blocks) ?? [];
102+
103+
for (const key of presetBlockSettingKeys) {
104+
if (!blockSchema.validSchema.settings?.some((setting) => setting.id === key)) {
105+
context.report({
106+
message: `Preset setting "${key}" does not exist in the block type "${block.type}"'s settings`,
107+
startIndex: 0,
108+
endIndex: 0,
109+
});
110+
}
111+
}
112+
}
113+
}
114+
}
115+
},
116+
};
117+
},
118+
};

packages/theme-check-node/configs/all.yml

+3
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ UnknownFilter:
106106
UnusedAssign:
107107
enabled: true
108108
severity: 1
109+
ValidBlockPresetSettings:
110+
enabled: true
111+
severity: 0
109112
ValidBlockTarget:
110113
enabled: true
111114
severity: 0

packages/theme-check-node/configs/recommended.yml

+3
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ UnusedAssign:
8787
ValidBlockTarget:
8888
enabled: true
8989
severity: 0
90+
ValidBlockPresetSettings:
91+
enabled: true
92+
severity: 0
9093
ValidContentForArguments:
9194
enabled: true
9295
severity: 0

0 commit comments

Comments
 (0)