mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-01 02:12:26 +00:00
Add design metrics tokens and tooling
This commit is contained in:
1 parent
0795393fb2
commit
afffb5c01e
12 files changed
+498
-24
No files matched your search
Executable
+168
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const repoRoot = path.resolve(__dirname, "..");
|
||||
|
||||
const TARGET_DIRECTORIES = ["js/ui"];
|
||||
const TARGET_FILES = ["js/channelProfile.js"];
|
||||
const IGNORED_EXTENSIONS = new Set([".min.js", ".map"]);
|
||||
const ALLOWED_FILES = new Set(["js/ui/components/RevertModal.js"]);
|
||||
const LENGTH_PATTERN = /\d+(?:\.\d+)?(?:px|rem)\b/gim;
|
||||
|
||||
function toPosix(relPath) {
|
||||
return relPath.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
async function collectTargetFiles() {
|
||||
const results = new Set();
|
||||
|
||||
for (const target of TARGET_DIRECTORIES) {
|
||||
const absolute = path.resolve(repoRoot, target);
|
||||
await walkDirectory(absolute, results);
|
||||
}
|
||||
|
||||
for (const file of TARGET_FILES) {
|
||||
const absolute = path.resolve(repoRoot, file);
|
||||
results.add(absolute);
|
||||
}
|
||||
|
||||
return [...results];
|
||||
}
|
||||
|
||||
async function walkDirectory(dir, results) {
|
||||
let entries = [];
|
||||
try {
|
||||
entries = await readdir(dir, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await walkDirectory(fullPath, results);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (!ext || IGNORED_EXTENSIONS.has(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
results.add(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
function buildLineBreaks(content) {
|
||||
const breaks = [];
|
||||
let index = content.indexOf("\n");
|
||||
while (index !== -1) {
|
||||
breaks.push(index);
|
||||
index = content.indexOf("\n", index + 1);
|
||||
}
|
||||
return breaks;
|
||||
}
|
||||
|
||||
function getLineNumber(lineBreaks, index) {
|
||||
let low = 0;
|
||||
let high = lineBreaks.length;
|
||||
while (low < high) {
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
if (index <= lineBreaks[mid]) {
|
||||
high = mid;
|
||||
} else {
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
return low + 1;
|
||||
}
|
||||
|
||||
function getLineSnippet(content, index) {
|
||||
const start = content.lastIndexOf("\n", index - 1) + 1;
|
||||
let end = content.indexOf("\n", index);
|
||||
if (end === -1) {
|
||||
end = content.length;
|
||||
}
|
||||
return content.slice(start, end).trim();
|
||||
}
|
||||
|
||||
function shouldSkipMatch(content, index) {
|
||||
const previousChar = index > 0 ? content[index - 1] : "";
|
||||
if (previousChar === "[") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function collectViolations(filePath, content) {
|
||||
const violations = [];
|
||||
const lineBreaks = buildLineBreaks(content);
|
||||
let match;
|
||||
LENGTH_PATTERN.lastIndex = 0;
|
||||
while ((match = LENGTH_PATTERN.exec(content))) {
|
||||
const index = match.index;
|
||||
if (shouldSkipMatch(content, index)) {
|
||||
continue;
|
||||
}
|
||||
const value = match[0];
|
||||
if (/^0+(?:\.0+)?(?:px|rem)$/i.test(value)) {
|
||||
continue;
|
||||
}
|
||||
violations.push({
|
||||
file: toPosix(path.relative(repoRoot, filePath)),
|
||||
line: getLineNumber(lineBreaks, index),
|
||||
value,
|
||||
snippet: getLineSnippet(content, index),
|
||||
});
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const files = await collectTargetFiles();
|
||||
const violations = [];
|
||||
|
||||
for (const filePath of files) {
|
||||
const rel = toPosix(path.relative(repoRoot, filePath));
|
||||
if (ALLOWED_FILES.has(rel)) {
|
||||
continue;
|
||||
}
|
||||
let content;
|
||||
try {
|
||||
content = await readFile(filePath, "utf8");
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
const fileViolations = collectViolations(filePath, content);
|
||||
violations.push(...fileViolations);
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("Design token lint failed: raw measurements detected outside tokens.\n");
|
||||
for (const violation of violations) {
|
||||
console.error(
|
||||
`${violation.file}:${violation.line} → ${violation.value}\n ${violation.snippet}`,
|
||||
);
|
||||
}
|
||||
console.error("\nMove these measurements into css/tokens.css or use the metrics helper.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Failed to check design tokens.");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -41,6 +41,7 @@ const IGNORED_FILES = new Set([
|
||||
|
||||
const VIOLATION_ALLOWLIST = new Map([
|
||||
["js/ui/utils/floatingPanelStyles.js", new Set(["Direct .style usage"])],
|
||||
["js/designSystem/metrics.js", new Set(["Direct .style usage"])],
|
||||
]);
|
||||
|
||||
const TEXT_EXTENSIONS = new Set([
|
||||
|
||||
Reference in new issue
Block a user