refactor(css): #6 后续阶段——@layer 级联 + token 化 + 行尾治理

- 引入 @layer ecommerce-core,standalone 覆盖层不再依赖 !important(全站 !important 7812→967)
This commit is contained in:
2026-06-16 12:23:50 +08:00
parent 003c41ddcc
commit 120fc2e70c
12 changed files with 8027 additions and 9017 deletions
+36
View File
@@ -0,0 +1,36 @@
/**
* Remove all !important declarations from a CSS file.
* For use with base.css and other files that no longer need !important
* because they are unlayered and override layered ecommerce.css by cascade priority.
*/
import { readFileSync, writeFileSync } from "node:fs";
const filePath = process.argv[2];
if (!filePath) {
console.error("Usage: node cleanup-css-important.mjs <file.css>");
process.exit(1);
}
let content = readFileSync(filePath, "utf-8");
// Strip BOM
content = content.replace(/^\uFEFF/, "");
// Remove !important
const before = (content.match(/!important/g) || []).length;
content = content.replace(/\s*!important/g, "");
// Fix artifacts
content = content.replace(/;\s*;/g, ";");
content = content.replace(/;\s*}/g, "}");
content = content.replace(/[ \t]+$/gm, "");
const after = (content.match(/!important/g) || []).length;
const lines = content.split("\n").length;
writeFileSync(filePath, content, "utf-8");
console.log(`Processed: ${filePath}`);
console.log(` Lines: ${lines}`);
console.log(` !important: ${before}${after} (removed ${before - after})`);
+79
View File
@@ -0,0 +1,79 @@
/**
* Clean up overrides.css:
* 1. Remove BOM character
* 2. Remove `html body #root ` specificity prefix from selectors
* 3. Remove `html body ` specificity prefix
* 4. Deduplicate doubled class selectors: .foo.foo → .foo
* 5. Deduplicate doubled attribute selectors: [attr="val"][attr="val"] → [attr="val"]
* 6. Remove all `!important` declarations
* 7. Compress multiple consecutive blank lines
*/
import { readFileSync, writeFileSync } from "node:fs";
const filePath = process.argv[2];
if (!filePath) {
console.error("Usage: node cleanup-css-overrides.mjs <file.css>");
process.exit(1);
}
let content = readFileSync(filePath, "utf-8");
// 0. Strip BOM
content = content.replace(/^\uFEFF/, "");
// 1. Remove `html body #root ` prefix (with space before the next selector)
content = content.replace(/^(\s*)html\s+body\s+#root\s+/gm, "$1");
// 2. Remove `html body ` prefix (remaining ones without #root)
content = content.replace(/^(\s*)html\s+body\s+/gm, "$1");
// 3. Deduplicate doubled class selectors: .foo.foo → .foo
// Must not match across word boundaries incorrectly
content = content.replace(/\.([\w-]+)(\.\1)\b/g, ".$1");
// 4. Deduplicate doubled attribute selectors: [data-tool="clone"][data-tool="clone"] → [data-tool="clone"]
// Use known patterns
const attrDedup = [
[/\[data-tool="clone"\]\[data-tool="clone"\]/g, '[data-tool="clone"]'],
[/\[data-tool="kit"\]\[data-tool="kit"\]/g, '[data-tool="kit"]'],
[/\[data-tool="video"\]\[data-tool="video"\]/g, '[data-tool="video"]'],
];
for (const [pat, repl] of attrDedup) {
content = content.replace(pat, repl);
}
// 4b. Generic dedup for any other doubled attribute patterns that slipped through
// Match [name] or [name="val"] followed by an exact copy
content = content.replace(/\[(\w[\w-]*(?:="[^"]*")?)\]\1/g, "[$1]");
// 5. Remove ` !important` — strip the space and the keyword, keeping the semicolon or newline
content = content.replace(/\s*!important/g, "");
// 6. Fix artifacts:
// - " ; " or " ;; " → "; "
content = content.replace(/;\s*;/g, ";");
// - Lone semicolon before closing brace: ";\n}" → "\n}" (just remove trailing ;)
content = content.replace(/;(\s*\})/g, "$1");
// - But keep "; " inside rule blocks — only clean up empty declarations
// 7. Don't collapse " ;}" — we already handled this above
// If after removing !important we have style: value; } → keep it as is
// 8. Compress multiple blank lines (>2 consecutive newlines) to at most one
content = content.replace(/\n{3,}/g, "\n\n");
// 9. Remove trailing whitespace on lines
content = content.replace(/[ \t]+$/gm, "");
// 10. Ensure single trailing newline
content = content.trimEnd() + "\n";
writeFileSync(filePath, content, "utf-8");
// Stats
const importantCount = (content.match(/!important/g) || []).length;
const lines = content.split("\n").length;
console.log(`Processed: ${filePath}`);
console.log(` Lines: ${lines}`);
console.log(` !important remaining: ${importantCount}`);
+3 -3
View File
@@ -70,9 +70,9 @@ console.log(
console.log("");
// Exit non-zero if total !important exceeds a budget threshold.
// Current baseline: ~7795. Set budget slightly above to allow incremental work
// while preventing uncontrolled growth.
const IMPORTANT_BUDGET = 7820;
// Post-@layer refactoring baseline: ~970 (formerly ~7812).
// Budget set to 2000 to prevent regression while allowing controlled growth.
const IMPORTANT_BUDGET = 2000;
if (totals.important > IMPORTANT_BUDGET) {
console.error(
`FAIL: !important count ${totals.important} exceeds budget ${IMPORTANT_BUDGET}. ` +
+93
View File
@@ -0,0 +1,93 @@
/**
* Replace hardcoded hex colors in base.css / overrides.css with
* CSS variable references defined in standalone/tokens.css.
*
* The replacement table maps exact hex values → var() references.
* Only standalone/*.css files are processed.
*/
import { readFileSync, writeFileSync } from "node:fs";
// Color → variable mappings (ordered so longer/more-specific colors are matched first)
const REPLACEMENTS = [
// Primary accent
["#1073cc", "var(--ecom-primary)"],
["#0f66b3", "var(--ecom-primary-hover)"],
// Cyan accent
["#1ebddb", "var(--ecom-accent-cyan)"],
["#1dbedb", "var(--ecom-accent-cyan)"], // common typo variant
["#16c8df", "var(--ecom-accent-cyan-light)"],
["#18bfd2", "var(--ecom-accent-cyan-bright)"],
["#0f829b", "var(--ecom-accent-cyan-deep)"],
// White/near-white
["#feffff", "var(--ecom-bg-near-white)"],
["#fbfdff", "var(--ecom-bg-near-white)"],
["#f8fdff", "var(--ecom-bg-near-white)"],
["#ffffff", "var(--ecom-bg-white)"],
["#fff", "var(--ecom-bg-white)"], // 3-digit short form
// Light backgrounds
["#f8f9fa", "var(--ecom-bg-page)"],
["#edf8fb", "var(--ecom-bg-tinted)"],
["#edf8ff", "var(--ecom-bg-tinted)"],
["#f3f8fa", "var(--ecom-bg-cool)"],
["#f8fbfc", "var(--ecom-bg-cool)"],
["#eef6f8", "var(--ecom-bg-cool)"],
// Dark blue backgrounds
["#172636", "var(--ecom-bg-dark-blue)"],
["#163447", "var(--ecom-bg-deep-blue)"],
["#083c67", "var(--ecom-bg-navy)"],
["#05233f", "var(--ecom-bg-navy-deep)"],
// Text
["#10202c", "var(--ecom-text-primary)"],
["#f5fbff", "var(--ecom-text-on-dark)"],
["#738392", "var(--ecom-text-muted)"],
["#71818e", "var(--ecom-text-muted)"],
["#66798a", "var(--ecom-text-muted)"],
["#526474", "var(--ecom-text-muted)"],
["#8da0ab", "var(--ecom-text-placeholder)"],
// Border
// NOTE: rgba() colors are NOT replaced — only hex values
];
function processFile(filePath) {
let content = readFileSync(filePath, "utf-8");
let replaced = 0;
for (const [hex, variable] of REPLACEMENTS) {
// Only replace when the hex appears as a CSS value (not inside comments or var())
// Match: colon-space-hex, comma-space-hex, space-hex, etc.
// But be careful: don't replace inside existing var() calls
const regex = new RegExp(
`(?<!var\\([^)]{0,200})${hex.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\da-fA-F])`,
"gi"
);
const matches = content.match(regex);
if (matches) {
replaced += matches.length;
content = content.replace(regex, variable);
}
}
writeFileSync(filePath, content, "utf-8");
console.log(` ${filePath}: ${replaced} replacements`);
return replaced;
}
const files = process.argv.slice(2);
if (files.length === 0) {
console.error("Usage: node tokenize-colors.mjs <files...>");
process.exit(1);
}
let total = 0;
for (const f of files) {
total += processFile(f);
}
console.log(`Total: ${total} replacements across ${files.length} file(s)`);