Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# For our testing scripts
/cache
/test/fixtures/actual-results.json
/test/fixtures-multithread

# For DS Store
**/.DS_Store
Expand Down
2 changes: 2 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@
// "esbenp.prettier-vscode", // Prettier for consistent formatting / not maintained and causes crashes https://github.com/prettier/prettier-vscode/issues/3743
"dbaeumer.vscode-eslint",
"arcanis.vscode-zipfs" // Required to use Prettier with Yarn in VS Code
,
"esbenp.prettier-vscode"
]
}
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
"license": "UNLICENSED",
"devDependencies": {
"@shopify/prettier-plugin-liquid": "^1.9.3",
"better-sqlite3": "^11.10.0",
"cheerio": "^1.1.0",
"better-sqlite3": "^12.2.0",
"cheerio": "^1.1.2",
"cli-progress": "^3.12.0",
"css": "^3.0.0",
"glob": "^11.0.2",
"html-validate": "^9.5.5",
"glob": "^11.0.3",
"html-validate": "^10.0.0",
"markdownlint-cli2": "^0.18.1",
"prettier": "^3.5.3",
"prettier": "^3.6.2",
"shell-quote": "^1.8.3"
},
"scripts": {
Expand Down
158 changes: 109 additions & 49 deletions test/build-html-validate.mjs
Original file line number Diff line number Diff line change
@@ -1,67 +1,127 @@
import { HtmlValidate, FileSystemConfigLoader, formatterFactory, esmResolver } from "html-validate";
import { glob } from "glob";
import cliProgress from "cli-progress";
import { Worker } from "worker_threads";
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// In the future, the CLI may improve and this script may be unnecessary.
// SEE: https://gitlab.com/html-validate/html-validate/-/issues/273

// Configuration
const MAX_WORKERS = parseInt(process.env.HTML_VALIDATE_WORKERS) || 4;
const WORKER_SCRIPT_PATH = path.join(__dirname, "html-validate-worker.mjs");

// Find and sort all HTML files in the 'build' directory
const targets = glob.sync("build/**/*.html").sort();

// Initialize HtmlValidate instance
const resolver = esmResolver();
const loader = new FileSystemConfigLoader([resolver]);
const htmlValidate = new HtmlValidate(loader);
const formatter = formatterFactory("stylish");
let allTestsPassed = true;

// Initialize progress bar
const bar = new cliProgress.SingleBar({
format: "πŸ§ͺ [{bar}] {percentage}% | {value}/{total} | ETA: {eta}s | {file}",
forceRedraw: true,
});
// Monkey-patch in logging support https://github.com/npkgz/cli-progress/issues/159#issuecomment-2959578474
bar.loggingBuffer = [];
bar.log = function (message) {
bar.loggingBuffer.push(message);
};
bar.on("redraw-pre", (data) => {
if (bar.loggingBuffer.length > 0) {
bar.terminal.clearLine();
bar.terminal.cursorTo(0);
while (bar.loggingBuffer.length > 0) {
bar.terminal.write(bar.loggingBuffer.shift());
bar.terminal.write("\n");
if (targets.length === 0) {
console.log("⚠️ No HTML files found in build directory");
console.log(" Make sure to build the site first");
process.exit(0);
}

console.log(`πŸ§ͺ Validating ${targets.length} files with ${MAX_WORKERS} parallel workers...`);

await validateParallel();

async function validateParallel() {
const multibar = new cliProgress.MultiBar({
format: "[{bar}] {percentage}% | {value}/{total} | {status}",
hideCursor: true,
clearOnComplete: false,
stopOnComplete: true,
forceRedraw: false,
}, cliProgress.Presets.shades_classic);

const overallBar = multibar.create(targets.length, 0, {
status: "Starting..."
});

let completedTasks = 0;
let allTestsPassed = true;
const results = [];
const workers = [];
const taskQueue = [...targets];

let isDone = false;
function completeParallelProcessing() {
if (isDone) return;
isDone = true;

multibar.stop();
workers.forEach(worker => worker.terminate());

const failedResults = results.filter(r => !r.isValid);
const passedCount = results.length - failedResults.length;

console.log("\nπŸ“Š Results summary:");
console.log(`βœ… ${passedCount} files passed validation`);

if (failedResults.length > 0) {
console.log(`❌ ${failedResults.length} files failed validation`);
process.exit(1);
} else {
console.log("✨ All tests passed!\n");
}
}
});

console.log("πŸ§ͺ Validating files...");
bar.start(targets.length, 0, { file: "Starting..." });
function createWorker(workerId) {
const worker = new Worker(WORKER_SCRIPT_PATH);

for (const target of targets) {
try {
bar.increment(0, { file: target });
const report = await htmlValidate.validateFile(target);
if (!report.valid) {
bar.log(formatter(report.results));
worker.on("message", (result) => {
completedTasks++;
results.push(result);

if (!result.isValid) {
allTestsPassed = false;
multibar.log(`❌ ${path.relative(process.cwd(), result.filePath)}\n${result.message.trim()}\n\n`);
}

overallBar.increment(1, {
status: path.basename(result.filePath),
});

if (taskQueue.length > 0) {
const nextTask = taskQueue.shift();
worker.postMessage({ filePath: nextTask, workerId });
}

if (completedTasks === targets.length) {
completeParallelProcessing();
}
});

worker.on("error", (error) => {
console.error(`Worker ${workerId} error:`, error);
allTestsPassed = false;
} else {
bar.log(`βœ… ${target}`);
}
} catch (error) {
bar.log(`❌ Error validating ${target}: ${error.message}`);
allTestsPassed = false;
completeParallelProcessing();
});

return worker;
}
bar.increment(1);
}

bar.stop();
function startParallelProcessing() {
for (let i = 0; i < MAX_WORKERS; i++) {
const worker = createWorker(i);
workers.push(worker);
}

const initialTasks = Math.min(MAX_WORKERS, taskQueue.length);
for (let i = 0; i < initialTasks; i++) {
const task = taskQueue.shift();
workers[i].postMessage({ filePath: task, workerId: i });
}
}

// Display final result
if (allTestsPassed) {
console.log("✨ All tests passed!\n");
} else {
console.log("❌ Some tests failed.\n");
process.exit(1);
return new Promise((resolve) => {
const originalComplete = completeParallelProcessing;
completeParallelProcessing = () => {
originalComplete();
resolve();
};
startParallelProcessing();
});
}
37 changes: 37 additions & 0 deletions test/html-validate-worker.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { parentPort, workerData } from "worker_threads";
import { HtmlValidate, FileSystemConfigLoader, formatterFactory, esmResolver } from "html-validate";

// Initialize HtmlValidate instance (same as main script)
const resolver = esmResolver();
const loader = new FileSystemConfigLoader([resolver]);
const htmlValidate = new HtmlValidate(loader);
const formatter = formatterFactory("text");

// Listen for messages from parent thread
parentPort.on("message", async (data) => {
const { filePath, workerId } = data;

try {
const report = await htmlValidate.validateFile(filePath);

const result = {
workerId,
filePath,
success: report.valid,
message: report.valid ? `βœ… ${filePath}` : formatter(report.results),
isValid: report.valid,
report: report
};

parentPort.postMessage(result);
} catch (error) { const result = {
workerId,
filePath,
success: false,
message: `❌ Error validating`,
isValid: false
};

parentPort.postMessage(result);
}
});
Loading
Loading