UNPKG

wallace-cli

Version:
254 lines (248 loc) 10.7 kB
#!/usr/bin/env node 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 {};