@dscodotco/theme-cli
Version:
A CLI tool for developing Shopify themes
152 lines (151 loc) • 5.59 kB
JavaScript
import { createLogger } from "../logger.js";
import { readFile } from "fs/promises";
import { join } from "path";
import { glob } from "glob";
const logger = createLogger("ThemeChecker");
export class ThemeChecker {
constructor(themeDir) {
this.themeDir = themeDir;
logger.info("Theme checker initialized successfully");
}
/**
* Check the entire theme for issues
*/
async checkTheme() {
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.message);
throw error;
}
}
/**
* Check specific files in the theme
*/
async checkFiles(files) {
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.message);
throw error;
}
}
/**
* Check a single file
*/
async checkFile(path, content) {
// 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
*/
formatResults(results) {
try {
const formatted = {
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.message);
throw error;
}
}
}