@dscodotco/theme-cli
Version:
A CLI tool for developing Shopify themes
221 lines (199 loc) • 5.78 kB
text/typescript
import { createLogger } from "../logger.js";
import { readFile } from "fs/promises";
import { join } from "path";
import { glob } from "glob";
const logger = createLogger("ThemeChecker");
interface CheckSummary {
errorCount: number;
warningCount: number;
suggestionCount: number;
checkedFiles: number;
}
interface CheckResult {
path: string;
offenses: Array<{
check: string;
message: string;
severity: "error" | "warning" | "suggestion";
start: { line: number; character: number };
end: { line: number; character: number };
}>;
}
interface FormattedResults {
errors: Array<{
file: string;
check: string;
message: string;
line: number;
column: number;
}>;
warnings: Array<{
file: string;
check: string;
message: string;
line: number;
column: number;
}>;
suggestions: Array<{
file: string;
check: string;
message: string;
line: number;
column: number;
}>;
summary: CheckSummary;
}
export class ThemeChecker {
private themeDir: string;
constructor(themeDir: string) {
this.themeDir = themeDir;
logger.info("Theme checker initialized successfully");
}
/**
* Check the entire theme for issues
*/
public async checkTheme(): Promise<FormattedResults> {
try {
const files = await glob("**/*.{liquid,json}", { cwd: this.themeDir });
const results = await Promise.all(
files.map(async (file) => {
const content = await readFile(join(this.themeDir, file), "utf-8");
return this.checkFile(file, content);
})
);
return this.formatResults(results);
} catch (error) {
logger.error("Theme check failed - " + (error as Error).message);
throw error;
}
}
/**
* Check specific files in the theme
*/
public async checkFiles(files: string[]): Promise<FormattedResults> {
try {
const results = await Promise.all(
files.map(async (file) => {
const content = await readFile(join(this.themeDir, file), "utf-8");
return this.checkFile(file, content);
})
);
return this.formatResults(results);
} catch (error) {
logger.error(
"File check failed for files [" +
files.join(", ") +
"] - " +
(error as Error).message
);
throw error;
}
}
/**
* Check a single file
*/
private async checkFile(path: string, content: string): Promise<CheckResult> {
// Basic checks for now
const offenses = [];
// Check for missing alt attributes on img tags
const imgRegex = /<img[^>]+>/g;
const altRegex = /alt=["'][^"']*["']/;
let match;
let line = 1;
let lastIndex = 0;
while ((match = imgRegex.exec(content)) !== null) {
if (!altRegex.test(match[0])) {
// Count lines up to this point
const textUpToMatch = content.slice(lastIndex, match.index);
line += (textUpToMatch.match(/\n/g) || []).length;
offenses.push({
check: "missing-alt",
message: "Image tag is missing alt attribute",
severity: "error",
start: { line, character: 0 },
end: { line, character: match[0].length },
});
}
lastIndex = match.index;
}
// Check for invalid Liquid syntax
const liquidRegex = /{%[^%}]*%}/g;
line = 1;
lastIndex = 0;
while ((match = liquidRegex.exec(content)) !== null) {
const tag = match[0];
if (!/^{%\s*[a-z]+[^%}]*%}$/i.test(tag)) {
// Count lines up to this point
const textUpToMatch = content.slice(lastIndex, match.index);
line += (textUpToMatch.match(/\n/g) || []).length;
offenses.push({
check: "invalid-liquid",
message: "Invalid Liquid syntax",
severity: "error",
start: { line, character: 0 },
end: { line, character: tag.length },
});
}
lastIndex = match.index;
}
return { path, offenses };
}
/**
* Format check results into a more usable structure
*/
private formatResults(results: CheckResult[]): FormattedResults {
try {
const formatted: FormattedResults = {
errors: [],
warnings: [],
suggestions: [],
summary: {
errorCount: 0,
warningCount: 0,
suggestionCount: 0,
checkedFiles: results.length,
},
};
for (const result of results) {
for (const offense of result.offenses) {
const offenseData = {
file: result.path,
check: offense.check,
message: offense.message,
line: offense.start.line,
column: offense.start.character,
};
switch (offense.severity) {
case "error":
formatted.errors.push(offenseData);
formatted.summary.errorCount++;
break;
case "warning":
formatted.warnings.push(offenseData);
formatted.summary.warningCount++;
break;
case "suggestion":
formatted.suggestions.push(offenseData);
formatted.summary.suggestionCount++;
break;
}
}
}
logger.info(
"Check results formatted successfully - " +
JSON.stringify({
errors: formatted.summary.errorCount,
warnings: formatted.summary.warningCount,
suggestions: formatted.summary.suggestionCount,
files: formatted.summary.checkedFiles,
})
);
return formatted;
} catch (error) {
logger.error(
"Failed to format check results - " + (error as Error).message
);
throw error;
}
}
}