Skip to content

Commit 6e23884

Browse files
committed
theme-check validation tool entire theme
1 parent 7693af0 commit 6e23884

File tree

6 files changed

+239
-10
lines changed

6 files changed

+239
-10
lines changed

package-lock.json

Lines changed: 67 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@modelcontextprotocol/sdk": "^1.15.1",
2121
"@shopify/theme-check-common": "^3.19.0",
2222
"@shopify/theme-check-docs-updater": "^3.19.0",
23+
"@shopify/theme-check-node": "^3.19.0",
2324
"graphql": "^16.11.0",
2425
"zod": "^3.24.2"
2526
},

src/tools/index.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ValidationToolResult } from "../types.js";
55
import { ValidationResult } from "../types.js";
66
import validateGraphQLOperation from "../validations/graphqlSchema.js";
77
import { hasFailedValidation } from "../validations/index.js";
8+
import validateTheme from "../validations/theme.js";
89
import validateThemeCodeblocks from "../validations/themeCodeBlock.js";
910
import { introspectGraphqlSchema } from "./introspectGraphqlSchema.js";
1011
import { shopifyDevFetch } from "./shopifyDevFetch.js";
@@ -569,13 +570,14 @@ function liquidMcpTools(server: McpServer) {
569570
),
570571
fileType: z
571572
.enum([
573+
"assets",
572574
"blocks",
573-
"snippets",
574-
"sections",
575-
"layout",
576575
"config",
576+
"layout",
577577
"locales",
578-
"assets",
578+
"sections",
579+
"snippets",
580+
"templates",
579581
])
580582
.default("blocks")
581583
.describe(
@@ -613,4 +615,38 @@ function liquidMcpTools(server: McpServer) {
613615
};
614616
},
615617
);
618+
619+
server.tool(
620+
"validate_theme",
621+
`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.
622+
623+
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.`,
624+
625+
withConversationId({
626+
absoluteThemePath: z
627+
.string()
628+
.describe("The absolute path to the theme directory"),
629+
}),
630+
631+
async (params) => {
632+
const validationResponse = await validateTheme(params.absoluteThemePath);
633+
634+
recordUsage("validate_theme", params, validationResponse).catch(() => {});
635+
636+
const responseText = formatValidationResult(
637+
[validationResponse],
638+
"Theme",
639+
);
640+
641+
return {
642+
content: [
643+
{
644+
type: "text" as const,
645+
text: responseText,
646+
},
647+
],
648+
isError: validationResponse.result === ValidationResult.FAILED,
649+
};
650+
},
651+
);
616652
}

src/validations/theme.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { mkdir, mkdtemp, rm, writeFile } from "fs/promises";
2+
import { tmpdir } from "os";
3+
import { join } from "path";
4+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
5+
import { ValidationResult } from "../types.js";
6+
import validateTheme from "./theme.js";
7+
8+
describe("validateTheme", () => {
9+
let tempThemeDirectory: string;
10+
let snippetsDirectory: string;
11+
let localesDirectory: string;
12+
13+
beforeEach(async () => {
14+
tempThemeDirectory = await mkdtemp(join(tmpdir(), "theme-test-"));
15+
16+
snippetsDirectory = join(tempThemeDirectory, "snippets");
17+
localesDirectory = join(tempThemeDirectory, "locales");
18+
19+
[snippetsDirectory, localesDirectory].forEach(async (directory) => {
20+
await mkdir(directory, { recursive: true });
21+
});
22+
});
23+
24+
afterEach(async () => {
25+
await rm(tempThemeDirectory, { recursive: true, force: true });
26+
});
27+
28+
it("should successfully validate a theme", async () => {
29+
// Create the test.liquid file with the specified content
30+
const liquidFile = join(snippetsDirectory, "test.liquid");
31+
await writeFile(liquidFile, "{{ 'hello' }}");
32+
33+
// Run validateTheme on the temporary directory
34+
const result = await validateTheme(tempThemeDirectory);
35+
36+
// Assert the response was a success
37+
expect(result.result).toBe(ValidationResult.SUCCESS);
38+
expect(result.resultDetail).toBe(
39+
`Theme at ${tempThemeDirectory} passed all checks from Shopify's Theme Check.`,
40+
);
41+
});
42+
43+
it("should fail to validate a theme", async () => {
44+
// Create the test.liquid file with the specified content
45+
const liquidFile = join(snippetsDirectory, "test.liquid");
46+
await writeFile(liquidFile, "{{ 'hello' | non-existent-filter }}");
47+
48+
// Run validateTheme on the temporary directory
49+
const result = await validateTheme(tempThemeDirectory);
50+
51+
// Assert the response was a success
52+
expect(result.result).toBe(ValidationResult.FAILED);
53+
expect(result.resultDetail).toBe(
54+
`Theme at ${tempThemeDirectory} failed to validate:
55+
56+
file://${tempThemeDirectory}/snippets/test.liquid: Unknown filter 'non-existent-filter' used.`,
57+
);
58+
});
59+
60+
it("should successfully validate a theme with an unknown filter if its check is exempted", async () => {
61+
// Create the test.liquid file with the specified content
62+
const liquidFile = join(snippetsDirectory, "test.liquid");
63+
await writeFile(liquidFile, "{{ 'hello' | non-existent-filter }}");
64+
65+
const themeCheckYml = join(tempThemeDirectory, ".theme-check.yml");
66+
await writeFile(themeCheckYml, "ignore:\n- snippets/test.liquid");
67+
68+
// Run validateTheme on the temporary directory
69+
const result = await validateTheme(tempThemeDirectory);
70+
71+
// Assert the response was a success
72+
expect(result.result).toBe(ValidationResult.SUCCESS);
73+
expect(result.resultDetail).toBe(
74+
`Theme at ${tempThemeDirectory} passed all checks from Shopify's Theme Check.`,
75+
);
76+
});
77+
});

src/validations/theme.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { themeCheckRun } from "@shopify/theme-check-node";
2+
import { access } from "fs/promises";
3+
import { join } from "path";
4+
import { ValidationResponse, ValidationResult } from "../types.js";
5+
6+
/**
7+
* Validates Shopify Theme
8+
* @param absoluteThemePath - The path to the theme directory
9+
* @returns ValidationResponse containing the success of running theme-check for the whole theme
10+
*/
11+
export default async function validateTheme(
12+
absoluteThemePath: string,
13+
): Promise<ValidationResponse> {
14+
try {
15+
let configPath: string | undefined = join(
16+
absoluteThemePath,
17+
"theme-check.yml",
18+
);
19+
20+
try {
21+
await access(configPath);
22+
} catch {
23+
configPath = undefined;
24+
}
25+
26+
const results = await themeCheckRun(
27+
absoluteThemePath,
28+
configPath,
29+
(message) => console.error(message),
30+
);
31+
32+
if (results.offenses.length > 0) {
33+
return {
34+
result: ValidationResult.FAILED,
35+
resultDetail: `Theme at ${absoluteThemePath} failed to validate:\n\n${results.offenses.map((offense) => `${offense.uri}: ${offense.message}`).join("\n\n")}`,
36+
};
37+
}
38+
39+
return {
40+
result: ValidationResult.SUCCESS,
41+
resultDetail: `Theme at ${absoluteThemePath} passed all checks from Shopify's Theme Check.`,
42+
};
43+
} catch (error) {
44+
return {
45+
result: ValidationResult.FAILED,
46+
resultDetail: `Validation error: Could not validate ${absoluteThemePath}. Details: ${error instanceof Error ? error.message : String(error)}`,
47+
};
48+
}
49+
}

src/validations/themeCodeBlock.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ import { ValidationResponse, ValidationResult } from "../types.js";
1616
type ThemeCodeblock = {
1717
fileName: string;
1818
fileType:
19+
| "assets"
1920
| "blocks"
20-
| "snippets"
21-
| "sections"
22-
| "layout"
2321
| "config"
22+
| "layout"
2423
| "locales"
25-
| "assets";
24+
| "sections"
25+
| "snippets"
26+
| "templates";
2627
content: string;
2728
};
2829

0 commit comments

Comments
 (0)