Skip to content

Commit 4fa0fc0

Browse files
committed
Support schema and doc errors in liquid tools
1 parent 49d3e4a commit 4fa0fc0

File tree

4 files changed

+161
-3
lines changed

4 files changed

+161
-3
lines changed

src/tools/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -557,9 +557,15 @@ function liquidMcpTools(server: McpServer) {
557557
return;
558558
}
559559

560+
const themeRepositoryDescription = `A theme repository is a directory that MUST contain the following directories: snippets, sections, config, templates. It can optionally contain assets, locales, blocks, layouts.`;
561+
560562
server.tool(
561563
"validate_theme_codeblocks",
562-
`This tool validates Liquid codeblocks, Liquid files, and supporting Theme files (e.g. JSON translation files, JSON config files, JSON template files, JavaScript files, CSS files, and SVG files) generated or updated by LLMs to ensure they don't have hallucinated Liquid filters, or incorrect references. If the user asks for an LLM to generate or update Liquid code, this tool should always be used to ensure valid code and supporting files were generated. If the codeblocks reference other files, those files must also be validated by this tool.`,
564+
`This tool validates Liquid codeblocks, Liquid files, and supporting Theme files (e.g. JSON locale files, JSON config files, JSON template files, JavaScript files, CSS files, and SVG files) generated or updated by LLMs to ensure they don't have hallucinated Liquid content, or incorrect references.
565+
566+
DO NOT use this tool if the user is asking to add, update, or delete files within a Theme repository - Use \`validate_theme\` instead. ${themeRepositoryDescription}
567+
568+
Provide every codeblock that was generated or updated by the LLM to this tool.`,
563569

564570
withConversationId({
565571
codeblocks: z
@@ -620,7 +626,7 @@ function liquidMcpTools(server: McpServer) {
620626

621627
server.tool(
622628
"validate_theme",
623-
`This tool MUST run if the user asks the LLM to create or modify Liquid code inside their Theme repository. A theme repository is a directory that MUST contain the following directories: snippets, sections, config, templates. It can optionally contain assets, locales, blocks, layouts.
629+
`This tool MUST run if the user asks the LLM to create or modify Liquid code inside their Theme repository. ${themeRepositoryDescription}
624630
625631
Only fix the errors in the files that are directly related to the user's prompt. Offer to fix other errors if the user asks for it.`,
626632

src/validations/theme.test.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@ import validateTheme from "./theme.js";
77

88
describe("validateTheme", () => {
99
let tempThemeDirectory: string;
10+
let blocksDirectory: string;
1011
let snippetsDirectory: string;
1112
let localesDirectory: string;
1213

1314
beforeEach(async () => {
1415
tempThemeDirectory = await mkdtemp(join(tmpdir(), "theme-test-"));
1516

17+
blocksDirectory = join(tempThemeDirectory, "blocks");
1618
snippetsDirectory = join(tempThemeDirectory, "snippets");
1719
localesDirectory = join(tempThemeDirectory, "locales");
1820

1921
await Promise.all([
22+
mkdir(blocksDirectory, { recursive: true }),
2023
mkdir(snippetsDirectory, { recursive: true }),
2124
mkdir(localesDirectory, { recursive: true }),
2225
]);
@@ -41,7 +44,7 @@ describe("validateTheme", () => {
4144
);
4245
});
4346

44-
it("should fail to validate a theme", async () => {
47+
it("should fail to validate a theme with an unknown filter", async () => {
4548
// Create the test.liquid file with the specified content
4649
const liquidFile = join(snippetsDirectory, "test.liquid");
4750
await writeFile(liquidFile, "{{ 'hello' | non-existent-filter }}");
@@ -56,6 +59,50 @@ describe("validateTheme", () => {
5659
);
5760
});
5861

62+
it("should fail to validate a theme with an invalid schema", async () => {
63+
// Create the test.liquid file with the specified content
64+
const liquidFile = join(blocksDirectory, "test.liquid");
65+
const schemaName = "Long long long long long long name";
66+
await writeFile(
67+
liquidFile,
68+
`
69+
{% schema %}
70+
{
71+
"name": "${schemaName}"
72+
}
73+
{% endschema %}`,
74+
);
75+
76+
// Run validateTheme on the temporary directory
77+
const result = await validateTheme(tempThemeDirectory);
78+
79+
// Assert the response was a success
80+
expect(result.result).toBe(ValidationResult.FAILED);
81+
expect(result.resultDetail).toContain(
82+
`Schema name '${schemaName}' is too long (max 25 characters)`,
83+
);
84+
});
85+
86+
it("should fail to validate a theme with LiquidDoc errors", async () => {
87+
const snippetFile = join(snippetsDirectory, "example-snippet.liquid");
88+
await writeFile(
89+
snippetFile,
90+
"{% doc %} @param {string} param {% enddoc %} {{ param }}",
91+
);
92+
93+
const liquidFile = join(blocksDirectory, "test.liquid");
94+
await writeFile(liquidFile, `{% render 'example-snippet' %}`);
95+
96+
// Run validateTheme on the temporary directory
97+
const result = await validateTheme(tempThemeDirectory);
98+
99+
// Assert the response was a success
100+
expect(result.result).toBe(ValidationResult.FAILED);
101+
expect(result.resultDetail).toContain(
102+
`Missing required argument 'param' in render tag for snippet 'example-snippet'.`,
103+
);
104+
});
105+
59106
it("should successfully validate a theme with an unknown filter if its check is exempted", async () => {
60107
// Create the test.liquid file with the specified content
61108
const liquidFile = join(snippetsDirectory, "test.liquid");

src/validations/themeCodeBlock.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,58 @@ describe("validateThemeCodeblocks", () => {
168168
"Theme codeblock test.liquid has the following offenses from using Shopify's Theme Check:\n\nERROR: 'snippets/non-existent-snippet.liquid' does not exist",
169169
});
170170
});
171+
172+
it("should fail to validate liquid code with schema errors", async () => {
173+
const schemaName = "Long long long long long long name";
174+
const codeblocks = [
175+
{
176+
fileName: "test.liquid",
177+
fileType: "blocks" as const,
178+
content: `
179+
{% schema %}
180+
{
181+
"name": "${schemaName}"
182+
}
183+
{% endschema %}`,
184+
},
185+
];
186+
187+
const result = await validateThemeCodeblocks(codeblocks);
188+
189+
expect(result).toContainEqual({
190+
result: ValidationResult.FAILED,
191+
resultDetail: `Theme codeblock test.liquid has the following offenses from using Shopify's Theme Check:
192+
193+
ERROR: Schema name '${schemaName}' is too long (max 25 characters)`,
194+
});
195+
});
196+
197+
it("should fail to validate liquid code with LiquidDoc errors", async () => {
198+
const codeblocks = [
199+
{
200+
fileName: "example-snippet.liquid",
201+
fileType: "snippets" as const,
202+
content: `
203+
{% doc %}
204+
@param {string} param
205+
{% enddoc %}
206+
{{ param }}
207+
`,
208+
},
209+
{
210+
fileName: "test.liquid",
211+
fileType: "blocks" as const,
212+
content: `{% render 'example-snippet' %}`,
213+
},
214+
];
215+
216+
const result = await validateThemeCodeblocks(codeblocks);
217+
218+
expect(result).toContainEqual({
219+
result: ValidationResult.FAILED,
220+
resultDetail: `Theme codeblock test.liquid has the following offenses from using Shopify's Theme Check:
221+
222+
ERROR: Missing required argument 'param' in render tag for snippet 'example-snippet'.; SUGGESTED FIXES: Add required argument 'param'`,
223+
});
224+
});
171225
});

src/validations/themeCodeBlock.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@ import {
22
AbstractFileSystem,
33
check,
44
Config,
5+
extractDocDefinition,
56
FileStat,
67
FileTuple,
78
FileType,
9+
LiquidHtmlNode,
810
Offense,
911
path,
1012
recommended,
13+
SectionSchema,
14+
SourceCodeType,
15+
ThemeBlockSchema,
16+
toSchema,
1117
toSourceCode,
1218
} from "@shopify/theme-check-common";
1319
import { ThemeLiquidDocsManager } from "@shopify/theme-check-docs-updater";
20+
import { normalize } from "path";
1421
import { ValidationResponse, ValidationResult } from "../types.js";
1522

1623
type ThemeCodeblock = {
@@ -95,6 +102,50 @@ async function runThemeCheck(theme: Theme) {
95102
fs: mockFs,
96103
themeDocset: docsManager,
97104
jsonValidationSet: docsManager,
105+
getBlockSchema: async (blockName) => {
106+
const blockUri = `file:///blocks/${blockName}.liquid`;
107+
const sourceCode = themeSourceCode.find((s) => s.uri === blockUri);
108+
109+
if (!sourceCode) {
110+
return undefined;
111+
}
112+
113+
return toSchema(
114+
"theme",
115+
blockUri,
116+
sourceCode,
117+
async () => true,
118+
) as Promise<ThemeBlockSchema | undefined>;
119+
},
120+
getSectionSchema: async (sectionName) => {
121+
const sectionUri = `file:///sections/${sectionName}.liquid`;
122+
const sourceCode = themeSourceCode.find((s) => s.uri === sectionUri);
123+
124+
if (!sourceCode) {
125+
return undefined;
126+
}
127+
128+
return toSchema(
129+
"theme",
130+
sectionUri,
131+
sourceCode,
132+
async () => true,
133+
) as Promise<SectionSchema | undefined>;
134+
},
135+
async getDocDefinition(relativePath) {
136+
const sourceCode = themeSourceCode.find((s) =>
137+
normalize(s.uri).endsWith(normalize(relativePath)),
138+
);
139+
140+
if (!sourceCode || sourceCode.type !== SourceCodeType.LiquidHtml) {
141+
return undefined;
142+
}
143+
144+
return extractDocDefinition(
145+
sourceCode.uri,
146+
sourceCode.ast as LiquidHtmlNode,
147+
);
148+
},
98149
});
99150
}
100151

0 commit comments

Comments
 (0)