Files
houseplan-card/scripts/dev/styles-diff.mjs
T
Codex cbdc248987 chore: add the #266 styles splitter and refactor-proof diff tools
styles-split.mjs — механический генератор слайсов (зоны в порядке финального
сборщика, инлайн-остаток промежуточных состояний сохраняет относительные
позиции финала); styles-diff.mjs — нормализованное множество правил с ключом
«полный путь вложенности + селектор» для доказательства refactor-only.

Issue: #266
User-Visible: no
2026-08-26 01:34:20 +03:00

71 lines
2.7 KiB
JavaScript

/**
* #266: refactor-only proof for the styles split. Extracts every css`...`
* template from the style sources (they contain no interpolations), parses
* the text into a normalised multiset of rules (media-scope + selector ->
* sorted declarations) and prints sorted JSON. Run on the base commit and on
* the branch, then diff the outputs: an empty diff proves the split moved
* rules without editing, adding or dropping a single declaration.
*
* node scripts/dev/styles-diff.mjs [files…] > after.json
*/
import { readFileSync } from 'node:fs';
// Default file set: src/styles.ts plus whatever surface files it actually
// imports — so intermediate slices are compared honestly, not against stale
// files left in src/styles/ by other runs. The multiset ignores order.
const detect = () => {
const aggregator = readFileSync('src/styles.ts', 'utf8');
const files = [];
if (/css`/.test(aggregator)) files.push('src/styles.ts');
for (const m of aggregator.matchAll(/from '\.\/styles\/([\w-]+)\.styles'/g)) {
files.push(`src/styles/${m[1]}.styles.ts`);
}
return files.length ? files : ['src/styles.ts'];
};
const files = process.argv.slice(2).length ? process.argv.slice(2) : detect();
const cssText = files.map((file) => {
const source = readFileSync(file, 'utf8');
const chunks = [...source.matchAll(/css`([\s\S]*?)`/g)].map((m) => m[1]);
if (!chunks.length) throw new Error(`${file}: no css\`\` template found`);
if (chunks.some((chunk) => chunk.includes('${'))) {
throw new Error(`${file}: interpolation found — the parser assumes plain CSS`);
}
return chunks.join('\n');
}).join('\n');
const text = cssText.replace(/\/\*[\s\S]*?\*\//g, '');
const rules = [];
const normalise = (s) => s.replace(/\s+/g, ' ').trim();
const walk = (chunk, scope) => {
let i = 0;
while (i < chunk.length) {
const open = chunk.indexOf('{', i);
if (open === -1) break;
const header = chunk.slice(i, open).trim();
let depth = 1;
let j = open + 1;
while (j < chunk.length && depth > 0) {
if (chunk[j] === '{') depth++;
else if (chunk[j] === '}') depth--;
j++;
}
const body = chunk.slice(open + 1, j - 1);
if (header.startsWith('@') && body.includes('{')) {
walk(body, `${scope} ${normalise(header)}`.trim());
} else {
rules.push({
scope,
selector: normalise(header),
declarations: body.split(';').map(normalise).filter(Boolean).sort(),
});
}
i = j;
}
};
walk(text, '');
rules.sort((a, b) => (a.scope + '|' + a.selector).localeCompare(b.scope + '|' + b.selector)
|| JSON.stringify(a.declarations).localeCompare(JSON.stringify(b.declarations)));
console.log(JSON.stringify(rules, null, 1));