UNPKG

@dscodotco/theme-cli

Version:

A CLI tool for developing Shopify themes

221 lines (199 loc) 5.78 kB
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; } } }