chore: add design tokens\css lint rules
This commit is contained in:
@@ -16,11 +16,15 @@ for (let i = 0; i < process.argv.length; i++) {
|
||||
}
|
||||
if (!viewportSeen) args.push("--viewport", VIEWPORTS.join(","));
|
||||
|
||||
const r = spawnSync(process.execPath, ["scripts/audit-cdp.mjs", ...args.slice(2)], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
const r = spawnSync(
|
||||
process.execPath,
|
||||
["scripts/audit-cdp.mjs", ...args.slice(2)],
|
||||
{
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
if (r.error) {
|
||||
console.error(r.error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(r.status ?? 1);
|
||||
process.exit(r.status ?? 1);
|
||||
|
||||
+36
-30
@@ -460,9 +460,10 @@ function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED, BOX_GROUPS }) {
|
||||
function expandFont(raw) {
|
||||
const v = raw.trim();
|
||||
// Locate the size token: a length/% literal or a clamp()/min()/max().
|
||||
const sz = /(\d+(?:\.\d+)?(?:px|em|rem|%|pt|vh|vw)|clamp\([^)]*\)|min\([^)]*\)|max\([^)]*\))/.exec(
|
||||
v,
|
||||
);
|
||||
const sz =
|
||||
/(\d+(?:\.\d+)?(?:px|em|rem|%|pt|vh|vw)|clamp\([^)]*\)|min\([^)]*\)|max\([^)]*\))/.exec(
|
||||
v,
|
||||
);
|
||||
if (!sz) return {};
|
||||
const size = sz[0];
|
||||
// Optional leading font-style/variant/weight/stretch prefix, then the
|
||||
@@ -550,7 +551,9 @@ function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED, BOX_GROUPS }) {
|
||||
// carrying var() was enumerated as EMPTY longhands, so it must be
|
||||
// re-parsed here to survive (e.g. border-radius: var(--radius)).
|
||||
if (map.has(name)) continue;
|
||||
for (const [ln, v] of Object.entries(expandShorthand(name, value, cs) || {}))
|
||||
for (const [ln, v] of Object.entries(
|
||||
expandShorthand(name, value, cs) || {},
|
||||
))
|
||||
add(ln, v, important);
|
||||
}
|
||||
return [...map.values()];
|
||||
@@ -693,32 +696,32 @@ function snapshotDocument({ PROPS, NOISE_TAGS, INHERITED, BOX_GROUPS }) {
|
||||
el.id === "svelte-announcer"
|
||||
)
|
||||
return null;
|
||||
// Computed style drives var() resolution and the recorded computed values;
|
||||
// computed once here and reused by the cascade matcher below.
|
||||
const cs = getComputedStyle(el);
|
||||
const specMap = new Map(inheritedEntries(inherited));
|
||||
for (const [name, d] of resolve(matchedDeclarations(el, rules, cs))) {
|
||||
const cur = specMap.get(name);
|
||||
if (!cur || betterThan(d, cur) > 0) specMap.set(name, d);
|
||||
}
|
||||
// Box shorthands (margin/padding/gap): record the ORIGINAL declared value
|
||||
// when one rule cleanly set every side (`consistent` cascade), so the report
|
||||
// can show "20px 20px 20px 20px" vs a single "20px" the way the CSS spells
|
||||
// it, not just the collapsed rendering.
|
||||
const groups = {};
|
||||
for (const g of BOX_GROUPS) {
|
||||
const declared = specMap.get(g.name)?.value;
|
||||
if (!declared) continue;
|
||||
const expanded = expandShorthand(g.name, declared, cs);
|
||||
const consistent = g.keys.every(
|
||||
(k) => (expanded[k] ?? "") === (specMap.get(k)?.value ?? ""),
|
||||
);
|
||||
groups[g.name] = { declared, consistent };
|
||||
}
|
||||
// Computed style drives var() resolution and the recorded computed values;
|
||||
// computed once here and reused by the cascade matcher below.
|
||||
const cs = getComputedStyle(el);
|
||||
const specMap = new Map(inheritedEntries(inherited));
|
||||
for (const [name, d] of resolve(matchedDeclarations(el, rules, cs))) {
|
||||
const cur = specMap.get(name);
|
||||
if (!cur || betterThan(d, cur) > 0) specMap.set(name, d);
|
||||
}
|
||||
// Box shorthands (margin/padding/gap): record the ORIGINAL declared value
|
||||
// when one rule cleanly set every side (`consistent` cascade), so the report
|
||||
// can show "20px 20px 20px 20px" vs a single "20px" the way the CSS spells
|
||||
// it, not just the collapsed rendering.
|
||||
const groups = {};
|
||||
for (const g of BOX_GROUPS) {
|
||||
const declared = specMap.get(g.name)?.value;
|
||||
if (!declared) continue;
|
||||
const expanded = expandShorthand(g.name, declared, cs);
|
||||
const consistent = g.keys.every(
|
||||
(k) => (expanded[k] ?? "") === (specMap.get(k)?.value ?? ""),
|
||||
);
|
||||
groups[g.name] = { declared, consistent };
|
||||
}
|
||||
|
||||
const styles = {};
|
||||
const raw = {};
|
||||
const computed = {};
|
||||
const styles = {};
|
||||
const raw = {};
|
||||
const computed = {};
|
||||
for (const p of PROPS) {
|
||||
const c = cs.getPropertyValue(p);
|
||||
computed[p] = c;
|
||||
@@ -1083,7 +1086,10 @@ function borderOf(a, b, n) {
|
||||
function borderLine(a, b) {
|
||||
const keys = [...BORDER_KEYS].filter((k) => isRealDiff(a, b, k));
|
||||
if (!keys.length) return null;
|
||||
return { line: `border: ${borderOf(a, b, a)} → ${borderOf(a, b, b)}`, keys };
|
||||
return {
|
||||
line: `border: ${borderOf(a, b, a)} → ${borderOf(a, b, b)}`,
|
||||
keys,
|
||||
};
|
||||
}
|
||||
|
||||
// CSS box shorthands reported as one line instead of one line per longhand
|
||||
|
||||
+83
-62
@@ -183,65 +183,64 @@ const keyOf = (n) =>
|
||||
n.children.length === 0 ? `${normTag(n.tag)}|${n.text}` : normTag(n.tag);
|
||||
|
||||
const snapshot = (page) =>
|
||||
page.evaluate(
|
||||
(styleFields) => {
|
||||
const sig = (el) => {
|
||||
const s = getComputedStyle(el);
|
||||
const o = {};
|
||||
for (const f of styleFields) o[f] = s[f];
|
||||
return o;
|
||||
page.evaluate((styleFields) => {
|
||||
const sig = (el) => {
|
||||
const s = getComputedStyle(el);
|
||||
const o = {};
|
||||
for (const f of styleFields) o[f] = s[f];
|
||||
return o;
|
||||
};
|
||||
const rectOf = (el) => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.round(r.x),
|
||||
y: Math.round(r.y),
|
||||
w: Math.round(r.width),
|
||||
h: Math.round(r.height),
|
||||
};
|
||||
const rectOf = (el) => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.round(r.x),
|
||||
y: Math.round(r.y),
|
||||
w: Math.round(r.width),
|
||||
h: Math.round(r.height),
|
||||
};
|
||||
};
|
||||
const readTokens = () => {
|
||||
const s = getComputedStyle(document.documentElement);
|
||||
const out = {};
|
||||
for (const k of s) if (k.startsWith("--")) out[k] = s.getPropertyValue(k).trim();
|
||||
return out;
|
||||
};
|
||||
const tokensLight = readTokens();
|
||||
const prev = document.documentElement.getAttribute("data-theme");
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
const tokensDark = readTokens();
|
||||
if (prev) document.documentElement.setAttribute("data-theme", prev);
|
||||
else document.documentElement.removeAttribute("data-theme");
|
||||
};
|
||||
const readTokens = () => {
|
||||
const s = getComputedStyle(document.documentElement);
|
||||
const out = {};
|
||||
for (const k of s)
|
||||
if (k.startsWith("--")) out[k] = s.getPropertyValue(k).trim();
|
||||
return out;
|
||||
};
|
||||
const tokensLight = readTokens();
|
||||
const prev = document.documentElement.getAttribute("data-theme");
|
||||
document.documentElement.setAttribute("data-theme", "dark");
|
||||
const tokensDark = readTokens();
|
||||
if (prev) document.documentElement.setAttribute("data-theme", prev);
|
||||
else document.documentElement.removeAttribute("data-theme");
|
||||
|
||||
const walk = (el) => {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (["script", "style", "noscript", "template"].includes(tag)) return null;
|
||||
if (el.id === "svelte-announcer") return null;
|
||||
if (el.hasAttribute("hidden")) return null;
|
||||
const cs = getComputedStyle(el);
|
||||
if (cs.display === "none" || cs.visibility === "hidden") return null;
|
||||
let ownText = "";
|
||||
for (const c of el.childNodes)
|
||||
if (c.nodeType === 3) ownText += c.textContent;
|
||||
ownText = ownText.replace(/\s+/g, " ").trim();
|
||||
const node = {
|
||||
tag,
|
||||
text: ownText,
|
||||
style: sig(el),
|
||||
rect: rectOf(el),
|
||||
children: [],
|
||||
};
|
||||
if (tag === "svg") return node;
|
||||
for (const child of el.children) {
|
||||
const cn = walk(child);
|
||||
if (cn) node.children.push(cn);
|
||||
}
|
||||
return node;
|
||||
const walk = (el) => {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (["script", "style", "noscript", "template"].includes(tag))
|
||||
return null;
|
||||
if (el.id === "svelte-announcer") return null;
|
||||
if (el.hasAttribute("hidden")) return null;
|
||||
const cs = getComputedStyle(el);
|
||||
if (cs.display === "none" || cs.visibility === "hidden") return null;
|
||||
let ownText = "";
|
||||
for (const c of el.childNodes)
|
||||
if (c.nodeType === 3) ownText += c.textContent;
|
||||
ownText = ownText.replace(/\s+/g, " ").trim();
|
||||
const node = {
|
||||
tag,
|
||||
text: ownText,
|
||||
style: sig(el),
|
||||
rect: rectOf(el),
|
||||
children: [],
|
||||
};
|
||||
return { tokensLight, tokensDark, tree: walk(document.body) };
|
||||
},
|
||||
STYLE_FIELDS,
|
||||
);
|
||||
if (tag === "svg") return node;
|
||||
for (const child of el.children) {
|
||||
const cn = walk(child);
|
||||
if (cn) node.children.push(cn);
|
||||
}
|
||||
return node;
|
||||
};
|
||||
return { tokensLight, tokensDark, tree: walk(document.body) };
|
||||
}, STYLE_FIELDS);
|
||||
|
||||
const TOK_NOISE = /^(--tw-|--lightningcss-|--default-)/;
|
||||
|
||||
@@ -284,12 +283,26 @@ function compareStyle(ours, ref, path, out) {
|
||||
const aNode = ours.children[i];
|
||||
const cands = bByKey.get(keyOf(aNode));
|
||||
let bIdx = -1;
|
||||
if (cands) for (const ci of cands) if (!used.has(ci)) { bIdx = ci; break; }
|
||||
if (cands)
|
||||
for (const ci of cands)
|
||||
if (!used.has(ci)) {
|
||||
bIdx = ci;
|
||||
break;
|
||||
}
|
||||
if (bIdx >= 0) {
|
||||
used.add(bIdx);
|
||||
compareStyle(aNode, ref.children[bIdx], `${path} > ${aNode.tag}:${i + 1}`, out);
|
||||
compareStyle(
|
||||
aNode,
|
||||
ref.children[bIdx],
|
||||
`${path} > ${aNode.tag}:${i + 1}`,
|
||||
out,
|
||||
);
|
||||
} else {
|
||||
out.push({ path: `${path} > ${aNode.tag}:${i + 1}`, type: "added", tag: aNode.tag });
|
||||
out.push({
|
||||
path: `${path} > ${aNode.tag}:${i + 1}`,
|
||||
type: "added",
|
||||
tag: aNode.tag,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (let j = 0; j < ref.children.length; j++) {
|
||||
@@ -425,7 +438,9 @@ function toMarkdown(reports) {
|
||||
: "_расхождений стилей нет_";
|
||||
const struct = r.deltas
|
||||
.filter((d) => d.type !== "style")
|
||||
.map((d) => `- \`${d.path}\` — **${d.type}**${d.tag ? ` (${d.tag})` : ""}`)
|
||||
.map(
|
||||
(d) => `- \`${d.path}\` — **${d.type}**${d.tag ? ` (${d.tag})` : ""}`,
|
||||
)
|
||||
.join("\n");
|
||||
return (
|
||||
`\n\n## ${r.route} (vs ${r.ref})\n\n` +
|
||||
@@ -480,7 +495,11 @@ async function main() {
|
||||
mkdirSync(resolve(WEB, "audit"), { recursive: true });
|
||||
writeFileSync(
|
||||
resolve(WEB, "audit/audit-report.json"),
|
||||
JSON.stringify({ generatedAt: new Date().toISOString(), reports }, null, 2),
|
||||
JSON.stringify(
|
||||
{ generatedAt: new Date().toISOString(), reports },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
writeFileSync(resolve(WEB, "audit/audit-report.md"), toMarkdown(reports));
|
||||
console.log(`report: web/audit/audit-report.md`);
|
||||
@@ -489,7 +508,9 @@ async function main() {
|
||||
if (server) {
|
||||
try {
|
||||
if (process.platform === "win32")
|
||||
spawn("taskkill", ["/pid", String(server.pid), "/f", "/t"], { stdio: "ignore" });
|
||||
spawn("taskkill", ["/pid", String(server.pid), "/f", "/t"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
else server.kill("SIGTERM");
|
||||
} catch {}
|
||||
}
|
||||
|
||||
+26
-11
@@ -143,7 +143,8 @@ const snapshot = (page) =>
|
||||
page.evaluate(() => {
|
||||
const walk = (el) => {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (["script", "style", "noscript", "template"].includes(tag)) return null;
|
||||
if (["script", "style", "noscript", "template"].includes(tag))
|
||||
return null;
|
||||
// SvelteKit ін'єктить #svelte-announcer (aria-live) прямо в <body>;
|
||||
// у рефа (Next.js) його немає — це фреймворк-шум, не збіг дизайну.
|
||||
if (el.id === "svelte-announcer") return null;
|
||||
@@ -189,9 +190,7 @@ const normTag = (tag) => (STRUCT_TAGS.has(tag) ? "box" : tag);
|
||||
// текст — иначе малейшее отличие текста в обёртке каскадом роняет всё поддерево.
|
||||
// Листья (нет дочерних элементов) матчим по тегу + тексту.
|
||||
const keyOf = (n) =>
|
||||
n.children.length === 0
|
||||
? `${normTag(n.tag)}|${n.ownText}`
|
||||
: normTag(n.tag);
|
||||
n.children.length === 0 ? `${normTag(n.tag)}|${n.ownText}` : normTag(n.tag);
|
||||
|
||||
function subtreeCount(node) {
|
||||
let c = 1;
|
||||
@@ -233,7 +232,12 @@ function matchChildren(ac, bc, path, out) {
|
||||
const k = keyOf(aNode);
|
||||
const cands = bByKey.get(k);
|
||||
let bIdx = -1;
|
||||
if (cands) for (const ci of cands) if (!used.has(ci)) { bIdx = ci; break; }
|
||||
if (cands)
|
||||
for (const ci of cands)
|
||||
if (!used.has(ci)) {
|
||||
bIdx = ci;
|
||||
break;
|
||||
}
|
||||
if (bIdx >= 0) {
|
||||
used.add(bIdx);
|
||||
diffNodes(aNode, bc[bIdx], `${path} > ${aNode.tag}:${i + 1}`, out);
|
||||
@@ -254,7 +258,14 @@ function diff(ours, ref, path, out) {
|
||||
}
|
||||
|
||||
function tally(deltas) {
|
||||
const c = { added: 0, removed: 0, tagMismatch: 0, textMismatch: 0, nodesAdded: 0, nodesRemoved: 0 };
|
||||
const c = {
|
||||
added: 0,
|
||||
removed: 0,
|
||||
tagMismatch: 0,
|
||||
textMismatch: 0,
|
||||
nodesAdded: 0,
|
||||
nodesRemoved: 0,
|
||||
};
|
||||
for (const d of deltas) {
|
||||
if (d.type in c) c[d.type]++;
|
||||
if (d.type === "added") c.nodesAdded += d.nodes ?? 1;
|
||||
@@ -317,10 +328,10 @@ function toMarkdown(reports) {
|
||||
return `- \`${d.path}\` **text**: ours \`${d.ours}\` → ref \`${d.ref}\``;
|
||||
if (d.type === "tagMismatch")
|
||||
return `- \`${d.path}\` **tag**: ours \`${d.ours}\` → ref \`${d.ref}\``;
|
||||
if (d.type === "added")
|
||||
return `- \`${d.path}\` **added** (\`${d.tag}\`${d.text ? ` "${d.text}"` : ""} · ${d.nodes} узл.)`;
|
||||
if (d.type === "removed")
|
||||
return `- \`${d.path}\` **removed** (\`${d.tag}\`${d.text ? ` "${d.text}"` : ""} · ${d.nodes} узл.)`;
|
||||
if (d.type === "added")
|
||||
return `- \`${d.path}\` **added** (\`${d.tag}\`${d.text ? ` "${d.text}"` : ""} · ${d.nodes} узл.)`;
|
||||
if (d.type === "removed")
|
||||
return `- \`${d.path}\` **removed** (\`${d.tag}\`${d.text ? ` "${d.text}"` : ""} · ${d.nodes} узл.)`;
|
||||
return "";
|
||||
})
|
||||
.join("\n")
|
||||
@@ -376,7 +387,11 @@ async function main() {
|
||||
mkdirSync(resolve(WEB, "audit"), { recursive: true });
|
||||
writeFileSync(
|
||||
resolve(WEB, "audit/dom-report.json"),
|
||||
JSON.stringify({ generatedAt: new Date().toISOString(), reports }, null, 2),
|
||||
JSON.stringify(
|
||||
{ generatedAt: new Date().toISOString(), reports },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
writeFileSync(resolve(WEB, "audit/dom-report.md"), toMarkdown(reports));
|
||||
console.log(`report: web/audit/dom-report.md`);
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// Token parity check: every color token defined in `:root` (light theme) of the
|
||||
// design CSS must also be defined in `[data-theme="dark"]`, and vice versa. A
|
||||
// color that exists only in one theme silently breaks dark mode.
|
||||
//
|
||||
// Usage: pnpm --dir web exec node scripts/check-token-parity.mjs
|
||||
// Exit code 1 (and a report) when tokens are missing.
|
||||
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import postcss from "postcss";
|
||||
|
||||
const FILE = new URL("../src/preview.css", import.meta.url);
|
||||
const SRC_DIR = fileURLToPath(new URL("../src/", import.meta.url));
|
||||
|
||||
// A var(--x) REFERENCE anywhere in the app (primary argument only).
|
||||
const USAGE_RE = /var\(\s*(--[\w-]+)/g;
|
||||
|
||||
const SCAN_EXTS = new Set([".svelte", ".css", ".ts", ".js", ".mjs"]);
|
||||
|
||||
// Recursively list source files; no glob dependency needed.
|
||||
function listFiles(dir) {
|
||||
const files = [];
|
||||
for (const name of readdirSync(dir)) {
|
||||
const full = join(dir, name);
|
||||
if (statSync(full).isDirectory()) {
|
||||
files.push(...listFiles(full));
|
||||
} else if (SCAN_EXTS.has(name.slice(name.lastIndexOf(".")))) {
|
||||
files.push(full);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
// Collect every token referenced via var() across all source files.
|
||||
function collectUsedTokens() {
|
||||
const used = new Set();
|
||||
for (const file of listFiles(SRC_DIR)) {
|
||||
const content = readFileSync(file, "utf8");
|
||||
for (const match of content.matchAll(USAGE_RE)) {
|
||||
used.add(match[1]);
|
||||
}
|
||||
}
|
||||
return used;
|
||||
}
|
||||
|
||||
// A token is "colorful" when its value is a color literal — those must have a
|
||||
// dark-mate. Non-color tokens (fonts, radii, durations) are exempt.
|
||||
const COLOR_RE =
|
||||
/#[0-9a-fA-F]{3,8}\b|\b(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklch|oklab)\s*\(/i;
|
||||
|
||||
const isColorValue = (value) => COLOR_RE.test(value);
|
||||
|
||||
// A token is "derived" when its value references var(...) — it adapts to the
|
||||
// theme automatically (e.g. --brand-alt: oklch(from var(--brand-main) ...)),
|
||||
// so it must NOT have a hardcoded dark twin.
|
||||
const isDerived = (value) => value.includes("var(");
|
||||
|
||||
// Collect { token: value } custom properties defined inside a given selector.
|
||||
function collectTokens(root, selector) {
|
||||
const map = new Map();
|
||||
root.walkRules((rule) => {
|
||||
if (rule.selector === selector) {
|
||||
rule.walkDecls((decl) => {
|
||||
if (decl.prop.startsWith("--")) map.set(decl.prop, decl.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const css = await readFile(FILE, "utf8");
|
||||
const root = postcss.parse(css);
|
||||
|
||||
const light = collectTokens(root, ":root");
|
||||
const dark = collectTokens(root, '[data-theme="dark"]');
|
||||
|
||||
const errors = [];
|
||||
|
||||
// Every colorful light token must have a dark-mate (derived tokens excluded).
|
||||
for (const [token, value] of light) {
|
||||
if (!isColorValue(value) || isDerived(value)) continue;
|
||||
if (!dark.has(token)) {
|
||||
errors.push(` ${token} in :root has no [data-theme="dark"] override`);
|
||||
}
|
||||
}
|
||||
|
||||
// Every dark token must exist in :root (a dark-only token is orphaned).
|
||||
for (const [token] of dark) {
|
||||
if (!light.has(token)) {
|
||||
errors.push(` ${token} in [data-theme="dark"] is missing in :root`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.log(`Token parity violations in ${FILE}:\n${errors.join("\n")}`);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(
|
||||
"Token parity: OK — all color tokens have both light/dark forms.",
|
||||
);
|
||||
}
|
||||
|
||||
// Unused design tokens: defined in preview.css but never used anywhere in
|
||||
// src via var(). A dead token is not the single source of truth — it's dust.
|
||||
const defined = new Set();
|
||||
root.walkDecls((decl) => {
|
||||
if (decl.prop.startsWith("--")) defined.add(decl.prop);
|
||||
});
|
||||
const used = collectUsedTokens();
|
||||
const unused = [...defined].filter((token) => !used.has(token));
|
||||
if (unused.length > 0) {
|
||||
console.log("\nUnused tokens (defined in preview.css, never used):");
|
||||
for (const token of unused) console.log(` ${token}`);
|
||||
} else {
|
||||
console.log("All preview.css tokens are used somewhere.");
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.log(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const WEB_DIR = fileURLToPath(new URL("../", import.meta.url));
|
||||
const node = process.execPath;
|
||||
|
||||
// (eslint, [args]) — every lint layer of the redesign, in one command.
|
||||
// Each step runs even if a previous one fails; exit code is nonzero if any.
|
||||
const steps = [
|
||||
{
|
||||
name: "ESLint (svelte) + design-tokens rules",
|
||||
args: [
|
||||
fileURLToPath(
|
||||
new URL("../node_modules/eslint/bin/eslint.js", import.meta.url),
|
||||
),
|
||||
".",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Stylelint (css) — design-token style",
|
||||
args: [
|
||||
fileURLToPath(
|
||||
new URL("../node_modules/stylelint/bin/stylelint.mjs", import.meta.url),
|
||||
),
|
||||
"./src/**/*.css",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Token audit (parity + unused)",
|
||||
args: [fileURLToPath(new URL("check-token-parity.mjs", import.meta.url))],
|
||||
},
|
||||
];
|
||||
|
||||
let failed = false;
|
||||
for (const [index, step] of steps.entries()) {
|
||||
console.log(`\n[${index + 1}/${steps.length}] ${step.name}`);
|
||||
const result = spawnSync(node, step.args, {
|
||||
cwd: WEB_DIR,
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (result.status !== 0) failed = true;
|
||||
}
|
||||
|
||||
console.log(
|
||||
failed
|
||||
? "\nlint-all: FAILED — fix the reported problems and re-run."
|
||||
: "\nlint-all: all checks passed.",
|
||||
);
|
||||
process.exit(failed ? 1 : 0);
|
||||
Reference in New Issue
Block a user