Skip to content

Commit 62f765e

Browse files
committed
Allow running partial test and lint, fixes #160
1 parent e127517 commit 62f765e

File tree

4 files changed

+165
-61
lines changed

4 files changed

+165
-61
lines changed

.github/workflows/code-lint.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
run: yarn install --immutable
3535

3636
- name: Check with prettier # Use yarn script instead of a prettier action, in order to load plugins from .prettierrc.js
37-
run: yarn run prettier-check
37+
run: yarn run lint:prettier
3838

3939
markdownlint:
4040
name: Markdownlint code lint
@@ -63,4 +63,4 @@ jobs:
6363
run: yarn install --immutable
6464

6565
- name: Check with markdownlint
66-
run: yarn run markdownlint-check
66+
run: yarn run lint:markdown

package.json

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
{
2+
"--": [
3+
"Notes:",
4+
"Our scripts use a special POSIX shim that allows you to run i.e. yarn markdownlint-check and specify files to check, or omit the files and it has a default.",
5+
"The magic is in the ${@:-\"**/*.md\"} part:",
6+
"$@: This represents all the arguments passed to the script (e.g., the files you want to lint).",
7+
"${...:-...}: This is the shell's 'use default value' operator.",
8+
"In plain English, it means: If $@ (the list of arguments) is empty or not set, use the default value **/*.md. Otherwise, use the arguments that were provided."
9+
],
210
"license": "UNLICENSED",
311
"devDependencies": {
412
"@fulldecent/nice-checkers-plugin": "^0.2.1",
@@ -11,15 +19,15 @@
1119
"prettier": "^3.6.2"
1220
},
1321
"scripts": {
22+
"lint": "sh -c 'yarn lint:prettier \"$@\" && yarn lint:markdown \"$@\"' --",
23+
"lint:prettier": "sh -c 'yarn prettier --check ${@:-\".\"}' --",
24+
"lint:markdown": "sh -c 'yarn markdownlint-cli2 ${@:-\"**/*.md\"}' --",
25+
"format-all": "yarn prettier --write . && yarn markdownlint-cli2 --fix '**/*.md'",
1426
"build": "bundle exec jekyll build",
15-
"test": "yarn node test/html-validate.mjs && yarn node test/dirty-file-paths-checker.mjs",
16-
"lint": "yarn prettier-check && yarn markdownlint-check",
17-
"lint-fix": "yarn prettier-fix && yarn markdownlint-fix",
1827
"generate-sitemap": "node scripts/generate-sitemap.mjs",
19-
"prettier-check": "yarn prettier --check .",
20-
"markdownlint-check": "yarn markdownlint-cli2 '**/*.md'",
21-
"prettier-fix": "yarn prettier --write .",
22-
"markdownlint-fix": "yarn markdownlint-cli2 --fix '**/*.md'",
28+
"test": "sh -c 'yarn test:html-validate \"$@\" && yarn test:dirty-file-paths-checker \"$@\"' --",
29+
"test:html-validate": "node test/html-validate.mjs",
30+
"test:dirty-file-paths-checker": "node test/dirty-file-paths-checker.mjs",
2331
"postinstall": "yarn dlx @yarnpkg/sdks vscode"
2432
},
2533
"packageManager": "[email protected]",

test/dirty-file-paths-checker.mjs

Lines changed: 93 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,82 @@
11
import fs from "fs";
22
import path from "path";
3-
import { glob } from "glob";
3+
import { globSync } from "glob";
44

55
const CONFIG_FILE = path.join(process.cwd(), "test", "dirty-file-paths.json");
66
const BUILD_DIR = path.join(process.cwd(), "build");
77

8-
// Load and parse the configuration file
8+
/**
9+
* Loads and parses the configuration file for dirty path rules.
10+
* @returns {object[]} The parsed configuration.
11+
*/
912
function loadConfig() {
13+
if (!fs.existsSync(CONFIG_FILE)) {
14+
console.error(`❌ Error: Configuration file not found at ${CONFIG_FILE}`);
15+
process.exit(1);
16+
}
1017
try {
1118
const configContent = fs.readFileSync(CONFIG_FILE, "utf-8");
1219
return JSON.parse(configContent);
1320
} catch (error) {
14-
console.error("Error loading configuration:", error.message);
21+
console.error("Error loading or parsing configuration:", error.message);
1522
process.exit(1);
1623
}
1724
}
1825

19-
// Find all files in the build directory
20-
function findTargetFiles() {
21-
return glob
22-
.sync("**/*", {
23-
cwd: BUILD_DIR,
24-
nocase: false, // Case sensitive as per requirements
25-
dot: false,
26-
})
27-
.filter((file) => {
28-
const fullPath = path.join(BUILD_DIR, file);
29-
return fs.lstatSync(fullPath).isFile();
26+
/**
27+
* Gathers target files from command-line arguments or the default build directory.
28+
* @returns {string[]} An array of absolute file paths to check.
29+
*/
30+
function getTargetFiles() {
31+
const args = process.argv.slice(2);
32+
33+
// If no arguments, default to all files in the build directory.
34+
if (args.length === 0) {
35+
return globSync(path.join(BUILD_DIR, "**", "*"), {
36+
nodir: true,
37+
absolute: true,
3038
});
39+
}
40+
41+
// Process arguments as file paths, directory paths, or glob patterns.
42+
const patterns = args.map((arg) => {
43+
try {
44+
if (fs.statSync(arg).isDirectory()) {
45+
// If it's a directory, create a glob to find all files within it.
46+
return path.join(arg, "**", "*");
47+
}
48+
} catch (e) {
49+
// Not a directory or doesn't exist, treat as a file/glob.
50+
}
51+
return arg;
52+
});
53+
54+
console.log(`ℹ️ Searching for files matching: ${patterns.join(", ")}`);
55+
return globSync(patterns, {
56+
nodir: true,
57+
absolute: true,
58+
});
3159
}
3260

33-
// Convert relative file path to the format expected by rules (starting with /)
61+
/**
62+
* Converts a file path to the format expected by rules (e.g., /path/to/file.html).
63+
* @param {string} filePath - The file path relative to the build directory.
64+
* @returns {string} The normalized path.
65+
*/
3466
function normalizePathForMatching(filePath) {
3567
// Convert backslashes to forward slashes and ensure it starts with /
3668
const normalized = filePath.replace(/\\/g, "/");
3769
return normalized.startsWith("/") ? normalized : "/" + normalized;
3870
}
3971

40-
// Check a single file's path against the dirty path rules
41-
function checkFile(filePath, config) {
42-
const normalizedPath = normalizePathForMatching(filePath);
43-
44-
// Process rules in order, later rules take precedence
72+
/**
73+
* Checks a single file's path against the dirty path rules.
74+
* @param {string} relativeFilePath - The path of the file relative to the build directory.
75+
* @param {object[]} config - The array of rules.
76+
* @returns {object | null} A violation object or null if the path is clean.
77+
*/
78+
function checkFile(relativeFilePath, config) {
79+
const normalizedPath = normalizePathForMatching(relativeFilePath);
4580
let finalRule = null;
4681

4782
for (const rule of config) {
@@ -51,12 +86,12 @@ function checkFile(filePath, config) {
5186
finalRule = rule;
5287
}
5388
} catch (error) {
54-
console.error(`Invalid regex pattern in rule: ${rule.path}`, error.message);
89+
console.error(`Invalid regex in rule: ${rule.path}`, error.message);
5590
process.exit(1);
5691
}
5792
}
5893

59-
// Return violation only if the final matching rule is dirty
94+
// Return a violation only if the final matching rule is 'dirty: true'.
6095
if (finalRule && finalRule.dirty) {
6196
return { path: normalizedPath, advice: finalRule.advice };
6297
}
@@ -67,32 +102,51 @@ function checkFile(filePath, config) {
67102
console.log("🧪 Testing files for dirty file paths");
68103

69104
const config = loadConfig();
70-
const files = findTargetFiles();
105+
const absoluteFilePaths = getTargetFiles();
71106
let hasErrors = false;
72-
const violationsByPath = new Map();
107+
const violations = [];
108+
const cleanFiles = [];
109+
110+
if (absoluteFilePaths.length === 0) {
111+
console.log("⚠️ No files found to check.");
112+
process.exit(0);
113+
}
114+
115+
// Collect all violations and clean files
116+
for (const absolutePath of absoluteFilePaths) {
117+
// Rules are based on the path relative to the site root (the build dir).
118+
const relativeToBuildDir = path.relative(BUILD_DIR, absolutePath);
119+
const violation = checkFile(relativeToBuildDir, config);
120+
121+
// Use a path relative to the current working directory for user-friendly logging.
122+
const relativeToCwd = path.relative(process.cwd(), absolutePath);
73123

74-
// Collect violations
75-
files.forEach((file) => {
76-
const violation = checkFile(file, config);
77124
if (violation) {
78125
hasErrors = true;
79-
console.log(`❌ ${file}: ${violation.advice}`);
80-
violationsByPath.set(violation.path, violation.advice);
126+
violations.push({ path: relativeToCwd, advice: violation.advice });
127+
} else {
128+
cleanFiles.push(relativeToCwd);
81129
}
82-
});
130+
}
83131

84-
// Summary of clean files
85-
const cleanFiles = files.filter((file) => !checkFile(file, config));
86-
console.log(`✅ Site includes ${cleanFiles.length} clean files`);
132+
// --- Report results ---
87133

88-
// Summary of dirty paths
134+
// First, log all the individual failures.
89135
if (hasErrors) {
90-
console.log("Following are dirty file paths:");
91-
violationsByPath.forEach((advice, path) => {
92-
console.log(`${path} ${advice}`);
136+
console.log("\n---");
137+
violations.forEach(({ path, advice }) => {
138+
console.log(`${path}: ${advice}`);
93139
});
94-
console.error("\n❌ Dirty paths check failed");
140+
console.log("---\n");
141+
}
142+
143+
console.log(`📊 Results Summary:`);
144+
console.log(`✅ Found ${cleanFiles.length} clean file paths.`);
145+
146+
if (hasErrors) {
147+
console.log(`❌ Found ${violations.length} dirty file paths.`);
148+
console.error("\n❌ Dirty paths check failed.");
95149
process.exit(1);
96150
} else {
97-
console.log("✨ All files passed dirty paths check!\n");
151+
console.log("✨ All file paths passed the check!\n");
98152
}

test/html-validate.mjs

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,64 @@
1-
import { glob } from "glob";
1+
import { globSync } from "glob";
22
import { Worker } from "worker_threads";
33
import path from "path";
44
import { fileURLToPath } from "url";
5+
import fs from "fs";
56

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

10+
// --- Configuration ---
911
// In the future, the CLI may improve and this script may be unnecessary.
1012
// SEE: https://gitlab.com/html-validate/html-validate/-/issues/273
11-
12-
// Configuration
1313
const MAX_WORKERS = parseInt(process.env.HTML_VALIDATE_WORKERS) || 4;
1414
const WORKER_SCRIPT_PATH = path.join(__dirname, "html-validate-worker.mjs");
1515

16-
// Find and sort all HTML files in the 'build' directory
17-
const targets = glob.sync("build/**/*.html").sort();
16+
/**
17+
* Gathers target HTML files from command-line arguments or a default directory.
18+
* @returns {string[]} A sorted and deduplicated array of HTML file paths.
19+
*/
20+
function getTargetFiles() {
21+
const args = process.argv.slice(2);
22+
23+
// If no arguments are provided, use the default glob pattern.
24+
if (args.length === 0) {
25+
console.log("ℹ️ No paths provided. Searching for HTML files in `build/` directory...");
26+
return globSync("build/**/*.html").sort();
27+
}
28+
29+
// If arguments are provided, process them into a list of glob patterns.
30+
const patterns = args.map((arg) => {
31+
try {
32+
// Check if the argument is a directory.
33+
if (fs.statSync(arg).isDirectory()) {
34+
// If it is, create a glob pattern to find all HTML files within it.
35+
return path.join(arg, "**", "*.html");
36+
}
37+
} catch (error) {
38+
// If fs.statSync fails, the path might not exist or isn't a directory.
39+
// In that case, we assume it's a file path or a glob pattern and use it directly.
40+
}
41+
// Return the argument as-is for glob to process.
42+
return arg;
43+
});
44+
45+
console.log(`ℹ️ Searching for files matching: ${patterns.join(", ")}`);
46+
// Use glob to find all files matching the generated patterns.
47+
const files = globSync(patterns, { nodir: true });
48+
49+
// Return a deduplicated and sorted list of files.
50+
return [...new Set(files)].sort();
51+
}
52+
53+
const targets = getTargetFiles();
1854

1955
if (targets.length === 0) {
2056
console.log("⚠️ No HTML files found in build directory");
2157
console.log(" Make sure to build the site first");
2258
process.exit(0);
2359
}
2460

25-
console.log(`🧪 Validating ${targets.length} files with ${MAX_WORKERS} parallel workers...`);
61+
console.log(`🧪 Validating ${targets.length} files with up to ${MAX_WORKERS} parallel workers...`);
2662

2763
await validateParallel();
2864

@@ -95,16 +131,18 @@ async function validateParallel() {
95131
}
96132

97133
function startParallelProcessing() {
98-
for (let i = 0; i < MAX_WORKERS; i++) {
134+
const workerCount = Math.min(MAX_WORKERS, taskQueue.length);
135+
for (let i = 0; i < workerCount; i++) {
99136
const worker = createWorker(i);
100137
workers.push(worker);
101138
}
102139

103-
const initialTasks = Math.min(MAX_WORKERS, taskQueue.length);
104-
for (let i = 0; i < initialTasks; i++) {
105-
const task = taskQueue.shift();
106-
workers[i].postMessage({ filePath: task, workerId: i });
107-
}
140+
workers.forEach((worker, i) => {
141+
if (taskQueue.length > 0) {
142+
const task = taskQueue.shift();
143+
worker.postMessage({ filePath: task, workerId: i });
144+
}
145+
});
108146
}
109147

110148
return new Promise((resolve) => {
@@ -113,6 +151,10 @@ async function validateParallel() {
113151
originalComplete();
114152
resolve();
115153
};
116-
startParallelProcessing();
154+
if (targets.length > 0) {
155+
startParallelProcessing();
156+
} else {
157+
completeParallelProcessing();
158+
}
117159
});
118160
}

0 commit comments

Comments
 (0)