mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
It looked for the tag written as `css` immediately followed by a backtick. The plugin is a Rollup transform, so the module has already been through TypeScript by the time it arrives, and the TS printer puts a space there: `css `. The guard therefore returned null for every stylesheet in the project, and minification never ran once — around 23 KB of explanatory comments went to every user in every release. Matching the tag as a word with optional whitespace turns it on: chunk, raw 1 079 508 -> 1 021 115 B (-58 393) initial view 300 111 -> 287 284 B gzip (-12 816) room to the budget 955 -> 13 782 B The ceiling moves down with the fact, as the tool asks when a graph shrinks past the band. The risk is not the two lines; it is that 23 KB of CSS is minified for the first time. Two witnesses cover it: a browser smoke that puts the original and the minified text into separate stylesheets and compares the serialised rules — 1 049 of them, identical up to the whitespace policy the minifier declares — and a test that takes real comment text out of src/styles and requires it to be absent from dist, so a plugin that silently stops working cannot pass again. Issue: #526 User-Visible: yes
111 lines
5.3 KiB
JavaScript
111 lines
5.3 KiB
JavaScript
// #526: включённая минификация не меняет смысл таблиц стилей.
|
|
//
|
|
// Минификатор молчал с самого начала (его отсев искал тег вплотную к бэктику,
|
|
// а плагин видит вывод TypeScript с пробелом), поэтому 23 КБ CSS проходят
|
|
// сжатие впервые. Риск задачи не в двух строках отсева, а в том, что где-то
|
|
// съеден значащий пробел — потомковый комбинатор, аргумент calc(), кавычка.
|
|
//
|
|
// Судим не глазами и не байтами, а браузером: исходный и минифицированный
|
|
// текст кладутся в отдельные CSSStyleSheet, и сериализованные правила обязаны
|
|
// совпасть посимвольно. Браузер сам приводит запись к канонической форме,
|
|
// поэтому совпадение cssText означает совпадение смысла.
|
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
import { minifyCssText } from '../scripts/css-template-minifier.mjs';
|
|
import { launch, checkAll, finish } from './serve.mjs';
|
|
|
|
/** Тело статического css-шаблона модуля (интерполяций в них нет, см. ТЗ). */
|
|
const templateBody = (source) => {
|
|
const match = /(^|[^\w$.])css\s*`/.exec(source);
|
|
if (!match) return null;
|
|
const start = match.index + match[0].length;
|
|
const end = source.indexOf('`', start);
|
|
if (end < 0) return null;
|
|
const body = source.slice(start, end);
|
|
return body.includes('\\') || body.includes('${') ? null : body;
|
|
};
|
|
|
|
const files = [
|
|
...readdirSync(new URL('../src/styles/', import.meta.url))
|
|
.filter((name) => name.endsWith('.ts'))
|
|
.map((name) => [`src/styles/${name}`, new URL(`../src/styles/${name}`, import.meta.url)]),
|
|
['src/editor-secondary.styles.ts', new URL('../src/editor-secondary.styles.ts', import.meta.url)],
|
|
];
|
|
|
|
const pairs = [];
|
|
for (const [label, url] of files) {
|
|
const body = templateBody(readFileSync(url, 'utf8'));
|
|
if (!body) continue;
|
|
pairs.push({ label, source: body, minified: minifyCssText(body, label) });
|
|
}
|
|
|
|
const { page, browser } = await launch();
|
|
const out = await page.evaluate(async (input) => {
|
|
const result = {};
|
|
// Минификатор по контракту схлопывает пробельные пробеги и убирает пробел
|
|
// после запятой. В обычных значениях браузер делает то же самое сам, но
|
|
// значение СВОЙСТВА-ПЕРЕМЕННОЙ он хранит текстом как есть, поэтому такие
|
|
// правила отличались бы записью при полном совпадении смысла. Сравнение
|
|
// нормализует ровно объявленную политику пробелов минификатора — пробельные
|
|
// пробеги и пробел рядом с `,`, `:`, `(`, `)` — и ничего больше: пропавший
|
|
// потомковый
|
|
// комбинатор (`.a .b` → `.a.b`) или съеденный пробел в `calc(100% - 2px)`
|
|
// остаются расхождением и валят свидетеля.
|
|
const normalize = (text) => text
|
|
.replace(/\s+/g, ' ')
|
|
.replace(/([,:(])\s/g, '$1')
|
|
.replace(/\s\)/g, ')');
|
|
const rulesOf = (sheet) => {
|
|
const list = [];
|
|
const walk = (rules) => {
|
|
for (const rule of rules) {
|
|
list.push(normalize(rule.cssText));
|
|
if (rule.cssRules) walk(rule.cssRules);
|
|
}
|
|
};
|
|
walk(sheet.cssRules);
|
|
return list;
|
|
};
|
|
const mismatches = [];
|
|
let comparedRules = 0;
|
|
for (const pair of input) {
|
|
const before = new CSSStyleSheet();
|
|
const after = new CSSStyleSheet();
|
|
before.replaceSync(pair.source);
|
|
after.replaceSync(pair.minified);
|
|
const left = rulesOf(before);
|
|
const right = rulesOf(after);
|
|
comparedRules += left.length;
|
|
if (left.length !== right.length) {
|
|
mismatches.push(`${pair.label}: правил ${left.length} против ${right.length}`);
|
|
continue;
|
|
}
|
|
for (let index = 0; index < left.length; index += 1) {
|
|
if (left[index] !== right[index]) {
|
|
mismatches.push(`${pair.label}: правило ${index}\n было: ${left[index].slice(0, 160)}\n стало: ${right[index].slice(0, 160)}`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
result.filesCompared = input.length;
|
|
result.enoughFiles = input.length >= 5;
|
|
result.rulesCompared = comparedRules;
|
|
result.enoughRules = comparedRules >= 500;
|
|
result.everyRuleIdentical = mismatches.length === 0;
|
|
result.mismatches = mismatches.slice(0, 3);
|
|
// Минификация обязана быть не просто безопасной, но и полезной.
|
|
const saved = input.reduce((sum, pair) => sum + pair.source.length - pair.minified.length, 0);
|
|
result.savedBytes = saved;
|
|
result.savesRealBytes = saved > 20000;
|
|
return result;
|
|
}, pairs);
|
|
|
|
console.log(JSON.stringify({ ...out, mismatches: out.mismatches }, null, 1));
|
|
checkAll(out, {
|
|
filesCompared: out.filesCompared,
|
|
rulesCompared: out.rulesCompared,
|
|
savedBytes: out.savedBytes,
|
|
mismatches: [],
|
|
});
|
|
await browser.close();
|
|
finish();
|