wallace-cli
Version:
Pretty CSS analytics on the CLI
254 lines (248 loc) • 10.7 kB
JavaScript
import { readFile } from "fs/promises";
import { join } from "path";
import { styleText } from "util";
import { analyze } from "@projectwallace/css-analyzer";
import { parseArgs } from "node:util";
//#region src/help.ts
function help(colors) {
return `
${colors.bold("Usage")}
${colors.dim("$")} wallace <path-to-file>
${colors.bold("Options")}
--json ${colors.italic("Format as JSON instead of a table")}
--help, -h Show this help
${colors.bold("Examples")}
# Point to a file
${colors.dim("$")} wallace path/to/styles.css
# CSS via stdin
${colors.dim("$")} cat style.css | wallace
# CSS from a server
${colors.dim("$")} curl http://localhost/css/style.css | wallace
# Format as json
${colors.dim("$")} wallace path/to/styles.css --json
`.trim();
}
//#endregion
//#region src/formatters.ts
let number_formatter = new Intl.NumberFormat();
/**
* Format byte size to a human readable size, like 100KB
* @see https://www.codexworld.com/how-to/convert-file-size-bytes-kb-mb-gb-javascript/
*/
function to_filesize(bytes) {
if (bytes == 0) return "0B";
const sizes = [
"B",
"KB",
"MB"
];
const step = 1e3;
const magnitude = Math.floor(Math.log(bytes) / Math.log(step));
return parseFloat((bytes / Math.pow(step, magnitude)).toFixed(1)) + sizes[magnitude];
}
function to_number(number, { decimals = 2 } = {}) {
return Number.isInteger(number) ? number_formatter.format(number) : number === 0 ? 0 : parseFloat(String(number)).toFixed(decimals);
}
function to_percentage(number) {
return String(parseFloat(String(number * 100)).toFixed(1)) + "%";
}
function pad_end(str, padLength, padString) {
return str + "".padEnd(padLength - str.length, padString);
}
function pad_start(str, padLength, padString) {
return "".padStart(padLength - str.length, padString) + str;
}
//#endregion
//#region src/components.ts
const columns = [
19,
12,
12,
12
];
const width = columns.reduce((total, num) => total += num, 0) + columns.length;
function Analytics(stats, style) {
function Row(...tds) {
return tds.map((td, index) => {
if (index === 0) return pad_end(String(td), columns[index]);
return pad_start(String(td), columns[index]);
}).join(" ");
}
function Hr() {
return style.dim("".padEnd(width, "─"));
}
function Summary(stats) {
return [
Hr(),
[
"Lines of Code",
"Filesize",
"Rules",
"Selectors",
"Declarations"
].join(style.dim(" │ ")),
[
style.bold(to_number(stats.stylesheet.sourceLinesOfCode).toString().padEnd(13)),
style.bold(to_filesize(stats.stylesheet.size).padEnd(8)),
style.bold(to_number(stats.rules.total).toString().padEnd(5)),
style.bold(to_number(stats.selectors.total).toString().padEnd(9)),
style.bold(to_number(stats.declarations.total).toString())
].join(style.dim(" │ ")),
Hr()
].join("\n");
}
function Stylesheet(stylesheet) {
return [Row("Comments", to_filesize(stylesheet.comments.size), style.dim(`(${stylesheet.comments.total} items)`)), Row("Embedded Content", to_filesize(stylesheet.embeddedContent.size.total), style.dim(`(${stylesheet.embeddedContent.types.total} items)`))].join("\n");
}
function Rules(rules) {
let empty_count = to_number(rules.empty.total);
return [
Row(style.underline("Rulesets"), style.dim("Most common"), style.dim("Average"), style.dim("Maximum")),
Row(`Selectors / rule`, to_number(rules.selectors.mode), to_number(rules.selectors.mean), to_number(rules.selectors.max)),
Row(`Declarations / rule`, to_number(rules.declarations.mode), to_number(rules.declarations.mean), to_number(rules.declarations.max)),
Row(`Empty rules`, "", "", rules.empty.total > 0 ? style.red(String(empty_count)) : empty_count)
].join("\n");
}
function Selectors(selectors) {
return [
Row(style.underline("Selectors"), style.dim("Most common"), style.dim("Average"), style.dim("Maximum")),
Row("Complexity", to_number(selectors.complexity.mode), to_number(selectors.complexity.mean), to_number(selectors.complexity.max)),
Row("Specificity", selectors.specificity.mode.join(style.dim("/")), selectors.specificity.mean.map((n) => to_number(n, { decimals: 1 })).join(style.dim("/")), selectors.specificity.max.join(style.dim("/"))),
Row(),
Row("", style.dim("Total"), style.dim("Unique"), style.dim("Ratio")),
Row(`All Selectors`, to_number(selectors.total), to_number(selectors.totalUnique), to_percentage(selectors.uniquenessRatio)),
Row(`ID Selectors`, to_number(selectors.id.total), to_number(selectors.id.totalUnique), to_percentage(selectors.id.uniquenessRatio)),
Row(`Accessibility`, to_number(selectors.accessibility.total), to_number(selectors.accessibility.totalUnique), to_percentage(selectors.accessibility.uniquenessRatio)),
Row(`Vendor prefixed`, to_number(selectors.prefixed.total), to_number(selectors.prefixed.totalUnique), to_percentage(selectors.prefixed.uniquenessRatio))
].join("\n");
}
function AtRules(atrules) {
const { media, supports, fontface, import: imports, keyframes, container, property } = atrules;
return [
Row(style.underline("AtRules"), style.dim("Total"), style.dim("Unique"), style.dim("Unique %")),
Row("@media", to_number(media.total), to_number(media.totalUnique), to_percentage(media.uniquenessRatio)),
Row("@supports", to_number(supports.total), to_number(supports.totalUnique), to_percentage(supports.uniquenessRatio)),
Row("@font-face", to_number(fontface.total), to_number(fontface.totalUnique), to_percentage(fontface.uniquenessRatio)),
Row("@import", to_number(imports.total), to_number(imports.totalUnique), to_percentage(imports.uniquenessRatio)),
Row("@keyframes", to_number(keyframes.total), to_number(keyframes.totalUnique), to_percentage(keyframes.uniquenessRatio)),
Row("@container", to_number(container.total), to_number(container.totalUnique), to_percentage(container.uniquenessRatio)),
Row("@property", to_number(property.total), to_number(property.totalUnique), to_percentage(property.uniquenessRatio))
].join("\n");
}
function Declarations(declarations) {
return [
Row(style.underline("Declarations"), style.dim("Total"), style.dim("Unique"), style.dim("Unique %")),
Row("All Declarations", to_number(declarations.total), to_number(declarations.totalUnique), to_percentage(declarations.uniquenessRatio)),
Row("!important", to_number(declarations.importants.total))
].join("\n");
}
function Properties(properties) {
return [
Row(style.underline("Properties"), style.dim("Total"), style.dim("Unique"), style.dim("Unique %")),
Row("All Properties", to_number(properties.total), to_number(properties.totalUnique), to_percentage(properties.uniquenessRatio)),
Row("Custom Properties", to_number(properties.custom.total), to_number(properties.custom.totalUnique), to_percentage(properties.custom.uniquenessRatio)),
Row("Vendor Prefixed", to_number(properties.prefixed.total), to_number(properties.prefixed.totalUnique), to_percentage(properties.prefixed.uniquenessRatio)),
Row("Browserhacks", to_number(properties.browserhacks.total), to_number(properties.browserhacks.totalUnique), to_percentage(properties.browserhacks.uniquenessRatio))
].join("\n");
}
function Values(values) {
function ValueRow(title, total, totalUnique, uniquenessRatio) {
return Row(title, to_number(total), to_number(totalUnique), to_percentage(uniquenessRatio));
}
const { colors, gradients, fontSizes, fontFamilies, lineHeights, textShadows, boxShadows, zindexes, prefixes, browserhacks, units, resets } = values;
return [
Row(style.underline("Values"), style.dim("Total"), style.dim("Unique"), style.dim("Unique %")),
ValueRow("Colors", colors.total, colors.totalUnique, colors.uniquenessRatio),
ValueRow("Gradients", gradients.total, gradients.totalUnique, gradients.uniquenessRatio),
ValueRow("Font-sizes", fontSizes.total, fontSizes.totalUnique, fontSizes.uniquenessRatio),
ValueRow("Font-families", fontFamilies.total, fontFamilies.totalUnique, fontFamilies.uniquenessRatio),
ValueRow("Line-heights", lineHeights.total, lineHeights.totalUnique, lineHeights.uniquenessRatio),
ValueRow("Text-shadows", textShadows.total, textShadows.totalUnique, textShadows.uniquenessRatio),
ValueRow("Box-shadows", boxShadows.total, boxShadows.totalUnique, boxShadows.uniquenessRatio),
ValueRow("Z-indexes", zindexes.total, zindexes.totalUnique, zindexes.uniquenessRatio),
ValueRow("Vendor Prefixed", prefixes.total, prefixes.totalUnique, prefixes.uniquenessRatio),
ValueRow("Browserhacks", browserhacks.total, browserhacks.totalUnique, browserhacks.uniquenessRatio),
ValueRow("Units", units.total, units.totalUnique, units.uniquenessRatio),
ValueRow("Resets", resets.total, resets.totalUnique, resets.uniquenessRatio)
].join("\n");
}
return [
Summary(stats),
Row(),
Stylesheet(stats.stylesheet),
Row(),
Rules(stats.rules),
Row(),
Selectors(stats.selectors),
Row(),
AtRules(stats.atrules),
Row(),
Declarations(stats.declarations),
Row(),
Properties(stats.properties),
Row(),
Values(stats.values)
].join("\n");
}
//#endregion
//#region src/program.ts
async function Program({ args, read_file, terminal_colors, stdin }) {
const { values, positionals } = parseArgs({
args,
options: {
json: {
type: "boolean",
short: "j"
},
help: {
type: "boolean",
short: "h"
}
},
allowPositionals: true,
strict: false
});
if (values.help || args.length === 0 && stdin === "") return help(terminal_colors);
const path_param = positionals[0];
const css = path_param ? await read_file(path_param) : stdin;
if (!css) return help(terminal_colors);
const stats = analyze(css, { useLocations: false });
delete stats.__meta__;
if (values.json) return JSON.stringify(stats);
return Analytics(stats, terminal_colors);
}
//#endregion
//#region src/bin.ts
const pc = {
bold: (str) => styleText("bold", str),
dim: (str) => styleText("dim", str),
italic: (str) => styleText("italic", str),
underline: (str) => styleText("underline", str),
red: (str) => styleText("red", str)
};
async function get_stdin() {
const { stdin } = process;
if (stdin.isTTY) return "";
let result = "";
for await (const chunk of stdin) result += chunk.toString();
return result;
}
async function read_file(path_param) {
return await readFile(join(process.cwd(), path_param), "utf-8");
}
async function main() {
const stdin = await get_stdin();
return Program({
args: process.argv.slice(2),
stdin,
read_file,
terminal_colors: pc
});
}
main().then(console.log).catch((error) => {
console.error(error.stack || error.message);
process.exitCode = 1;
});
//#endregion
export {};