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
112 lines
3.3 KiB
JavaScript
112 lines
3.3 KiB
JavaScript
/**
|
|
* Conservative minifier for static Lit css`` templates.
|
|
*
|
|
* This deliberately is not a general CSS optimiser: it removes comments and
|
|
* redundant whitespace while preserving strings, escapes and descendant
|
|
* combinators. Interpolated templates fail closed because rewriting across a
|
|
* JavaScript expression would make the build depend on runtime values.
|
|
*/
|
|
const isWhitespace = (char) => /[\t\n\f\r ]/.test(char);
|
|
const TIGHT_BEFORE = new Set(['{', '}', ':', ';', ',', '>', ')']);
|
|
const TIGHT_AFTER = new Set(['{', '}', ':', ';', ',', '>', '(']);
|
|
|
|
export function minifyCssText(source, label = '<css>') {
|
|
let out = '';
|
|
let quote = '';
|
|
let pendingSpace = false;
|
|
|
|
for (let i = 0; i < source.length; i += 1) {
|
|
const char = source[i];
|
|
const next = source[i + 1];
|
|
|
|
if (quote) {
|
|
out += char;
|
|
if (char === '\\') {
|
|
if (i + 1 >= source.length) throw new Error(`${label}: dangling CSS escape`);
|
|
out += source[++i];
|
|
} else if (char === quote) quote = '';
|
|
continue;
|
|
}
|
|
|
|
if (char === '"' || char === "'") {
|
|
if (pendingSpace && out && !TIGHT_AFTER.has(out.at(-1))) out += ' ';
|
|
pendingSpace = false;
|
|
quote = char;
|
|
out += char;
|
|
continue;
|
|
}
|
|
|
|
if (char === '/' && next === '*') {
|
|
const end = source.indexOf('*/', i + 2);
|
|
if (end < 0) throw new Error(`${label}: unclosed CSS comment`);
|
|
i = end + 1;
|
|
continue;
|
|
}
|
|
|
|
if (isWhitespace(char)) {
|
|
pendingSpace = true;
|
|
continue;
|
|
}
|
|
|
|
if (pendingSpace && out && !TIGHT_AFTER.has(out.at(-1)) && !TIGHT_BEFORE.has(char)) {
|
|
out += ' ';
|
|
}
|
|
pendingSpace = false;
|
|
if (TIGHT_BEFORE.has(char) && out.endsWith(' ')) out = out.slice(0, -1);
|
|
out += char;
|
|
}
|
|
|
|
if (quote) throw new Error(`${label}: unclosed CSS string`);
|
|
return out.trim();
|
|
}
|
|
|
|
const CSS_TAG = /(^|[^\w$.])css\s*`/;
|
|
|
|
export function minifyStaticCssTemplates(code, id = '<module>') {
|
|
let cursor = 0;
|
|
let output = '';
|
|
let changed = false;
|
|
while (true) {
|
|
const match = CSS_TAG.exec(code.slice(cursor));
|
|
if (!match) break;
|
|
const start = cursor + match.index + match[1].length;
|
|
const tagLength = match[0].length - match[1].length;
|
|
let end = start + tagLength;
|
|
let escaped = false;
|
|
for (; end < code.length; end += 1) {
|
|
const char = code[end];
|
|
if (escaped) {
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
if (char === '\\') {
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
if (char === '$' && code[end + 1] === '{') {
|
|
throw new Error(`${id}:${end}: interpolated css template is not supported`);
|
|
}
|
|
if (char === '`') break;
|
|
}
|
|
if (end >= code.length) throw new Error(`${id}:${start}: unclosed css template`);
|
|
const css = code.slice(start + tagLength, end);
|
|
output += code.slice(cursor, start);
|
|
output += `css\`${minifyCssText(css, `${id}:${start}`)}\``;
|
|
cursor = end + 1;
|
|
changed = true;
|
|
}
|
|
if (!changed) return null;
|
|
return output + code.slice(cursor);
|
|
}
|
|
|
|
export function cssTemplateMinifier() {
|
|
return {
|
|
name: 'houseplan-css-template-minifier',
|
|
transform(code, id) {
|
|
if (!id.endsWith('.ts') || !CSS_TAG.test(code)) return null;
|
|
const transformed = minifyStaticCssTemplates(code, id);
|
|
return transformed == null ? null : { code: transformed, map: null };
|
|
},
|
|
};
|
|
}
|