├── .eleventy.js ├── .gitignore ├── LICENSE ├── README.md └── package.json /.eleventy.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { gray, green, yellow } = require("kleur"); 3 | const stripColor = require("strip-color"); 4 | 5 | // TODO move these into plugin options instead 6 | const FOLDER_ICON = "↘ "; 7 | const FOLDER_ICON_SINGLE = "→ "; 8 | const FILE_ICON = "• "; 9 | 10 | // TODO move these into plugin options instead 11 | const MAX_FOLDER_LENGTH = 20; 12 | const MAX_FILENAME_LENGTH = 30; 13 | 14 | const SPECIAL_FILE_KEY = "file:" 15 | 16 | // TODO remove input directory from every entry in column 2 17 | // TODO show preprocessor template language (shown in verbose output) e.g. .md files show (liquid) 18 | 19 | function _pad(str, size, mode = "left") { 20 | let colorOffset = str.length - stripColor(str).length; 21 | size += colorOffset; 22 | 23 | let whitespace = Array.from({length: size}).join(" ") + " "; 24 | if(mode === "left") { 25 | return (str + whitespace).substr(0, size); 26 | } 27 | let result = whitespace + str; 28 | return result.substr(result.length - size); 29 | } 30 | 31 | function padLeftAlign(str, size) { 32 | return _pad(str, size, "left"); 33 | } 34 | 35 | function padRightAlign(str, size) { 36 | return _pad(str, size, "right"); 37 | } 38 | 39 | function truncate(str, maxLength) { 40 | if(maxLength && str.length > maxLength) { 41 | return str.substr(0, maxLength) + "[…]"; 42 | } 43 | return str; 44 | } 45 | 46 | class Directory { 47 | constructor(options) { 48 | this.output = []; 49 | this.compileBenchmarksReported = {}; 50 | this.options = options; 51 | } 52 | 53 | setConfigDirectories(dirs) { 54 | this.dirs = dirs; 55 | } 56 | 57 | print() { 58 | let colMax = [0, 0, 0, 0]; 59 | for(let line of this.output) { 60 | let [location, inputFile, size, renderTime] = line; 61 | colMax[0] = Math.max(stripColor(location).length, colMax[0]); 62 | if(inputFile) { 63 | colMax[1] = Math.max(stripColor(inputFile).length, colMax[1]); 64 | } 65 | if(size) { 66 | colMax[2] = Math.max(stripColor(size).length, colMax[2]); 67 | } 68 | if(renderTime) { 69 | colMax[3] = Math.max(stripColor(renderTime).length, colMax[3]); 70 | } 71 | } 72 | 73 | for(let line of this.output) { 74 | let [location, inputFile, size, renderTime] = line; 75 | let cols = [ 76 | padLeftAlign(location, colMax[0] + 2), 77 | padLeftAlign(inputFile ? `${inputFile}` : gray("--"), colMax[1] + 2), 78 | ]; 79 | // TODO yellow/red color if larger than X KB 80 | if(this.options.columns && this.options.columns.filesize !== false) { 81 | cols.push(padRightAlign(size || gray("--"), colMax[2] + 2)); 82 | } 83 | if(this.options.columns && this.options.columns.benchmark !== false) { 84 | cols.push(padRightAlign(renderTime ? renderTime : gray("--"), colMax[3] + 2)); 85 | } 86 | 87 | console.log( 88 | ...cols 89 | ); 90 | } 91 | } 92 | 93 | displayTime(ms) { 94 | return !isNaN(ms) ? ms.toFixed(1) + "ms" : ""; 95 | } 96 | 97 | displayFileSize(size) { 98 | let sizeStr = (size / 1000).toFixed(1) + "kB"; 99 | if(size && size > this.options.warningFileSize) { 100 | return yellow(sizeStr); 101 | } 102 | return sizeStr; 103 | } 104 | 105 | static normalizeLocation(location) { 106 | let result = {}; 107 | let parsed = path.parse(location); 108 | 109 | let targetDir = parsed.dir; 110 | if(targetDir.startsWith("." + path.sep)) { 111 | targetDir = targetDir.substr(2); 112 | } else if(targetDir === ".") { 113 | targetDir = ""; 114 | } 115 | 116 | result.dir = targetDir; 117 | 118 | if(result.dir.startsWith(path.sep)) { 119 | result.dir = result.dir.substr(1); 120 | } 121 | result.dir = result.dir.split(path.sep).map(entry => { 122 | return truncate(entry, MAX_FOLDER_LENGTH); 123 | }).join(path.sep); 124 | 125 | result.filename = truncate(parsed.name, MAX_FILENAME_LENGTH) + parsed.ext; 126 | 127 | return result; 128 | } 129 | 130 | // Hacky hack: For pagination templates, they are only compiled once per input file 131 | // so we just add to the first entry. 132 | _getCompileTime(meta) { 133 | let compileTime = 0; 134 | let key = meta.input.dir + path.sep + meta.input.filename; 135 | if(meta.benchmarks.compile && !this.compileBenchmarksReported[key]) { 136 | compileTime = meta.benchmarks.compile; 137 | 138 | this.compileBenchmarksReported[key] = true; 139 | } 140 | return compileTime; 141 | } 142 | 143 | getFileColumns(meta, depth = 0, prefix = "", icon = FILE_ICON) { 144 | let filename = meta.output.filename; 145 | if(prefix && filename.startsWith("index.html")) { 146 | filename = gray(filename); 147 | } 148 | 149 | let compileTime = this._getCompileTime(meta); 150 | return [ 151 | `${padLeftAlign("", depth)}${icon}${prefix}${filename}`, 152 | `${meta.input.dir ? `${meta.input.dir}/` : ""}${meta.input.filename}`, 153 | this.displayFileSize(meta.size), 154 | this.displayTime(compileTime + meta.benchmarks.render), 155 | ]; 156 | } 157 | 158 | static sortByKeys(obj) { 159 | let sorted = {}; 160 | let keys = Object.keys(obj).sort((a, b) => { 161 | if(a.startsWith(SPECIAL_FILE_KEY) && !b.startsWith(SPECIAL_FILE_KEY)) { 162 | return 1; 163 | } 164 | if(b.startsWith(SPECIAL_FILE_KEY) && !a.startsWith(SPECIAL_FILE_KEY)) { 165 | return -1; 166 | } 167 | if(a < b) { 168 | return -1; 169 | } 170 | if(b > a) { 171 | return 1; 172 | } 173 | return 0; 174 | }); 175 | 176 | for(let key of keys) { 177 | sorted[key] = obj[key]; 178 | } 179 | return sorted; 180 | } 181 | 182 | parseResults(obj, depth = 0) { 183 | let sorted = Directory.sortByKeys(obj); 184 | for(let name in sorted) { 185 | let meta = sorted[name]; 186 | if(name.startsWith(SPECIAL_FILE_KEY)) { 187 | let cols = this.getFileColumns(meta, depth); 188 | this.output.push(cols); 189 | } else { 190 | let children = Object.keys(meta); 191 | let files = children.filter(entry => entry.startsWith(SPECIAL_FILE_KEY)); 192 | if(children.length === 1 && files.length === 1) { 193 | let childFile = meta[files[0]]; 194 | let cols = this.getFileColumns(childFile, depth, green(name + "/"), FOLDER_ICON_SINGLE); 195 | this.output.push(cols); 196 | } else { 197 | let cols = [ 198 | `${padLeftAlign("", depth)}${FOLDER_ICON}${green(name + "/")}` 199 | ]; 200 | this.output.push(cols); 201 | 202 | this.parseResults(meta, depth + 2); 203 | } 204 | } 205 | } 206 | } 207 | } 208 | 209 | module.exports = function(eleventyConfig, opts = {}) { 210 | let options = Object.assign({ 211 | warningFileSize: 400 * 1000, // bytes 212 | columns: {} 213 | }, opts); 214 | 215 | let configDirs = {}; 216 | eleventyConfig.on("eleventy.directories", function(dirs) { 217 | configDirs = dirs; 218 | }); 219 | 220 | let results = {}; 221 | eleventyConfig.on("eleventy.before", function() { 222 | results = {}; 223 | }); 224 | eleventyConfig.on("eleventy.after", function() { 225 | let d = new Directory(options); 226 | d.setConfigDirectories(configDirs); 227 | d.parseResults(results); 228 | d.print(); 229 | }); 230 | 231 | function getBenchmarks(inputPath, outputPath) { 232 | let benchmarks = {}; 233 | let keys = { 234 | render: `> Render > ${outputPath}`, 235 | compile: `> Compile > ${inputPath}`, 236 | }; 237 | 238 | if(eleventyConfig.benchmarkManager) { 239 | let benchmarkGroup = eleventyConfig.benchmarkManager.get("Aggregate"); 240 | 241 | if("has" in benchmarkGroup && benchmarkGroup.has(keys.render)) { 242 | let b1 = benchmarkGroup.get(keys.render); 243 | benchmarks.render = b1.getTotal(); 244 | } 245 | 246 | if("has" in benchmarkGroup && benchmarkGroup.has(keys.compile)) { 247 | let b2 = benchmarkGroup.get(keys.compile); 248 | benchmarks.compile = b2.getTotal(); 249 | } 250 | } 251 | 252 | return benchmarks; 253 | } 254 | 255 | eleventyConfig.addLinter("directory-output", function(content) { 256 | if(this.outputPath === false || typeof content !== "string") { 257 | return; 258 | } 259 | let inputLocation = Directory.normalizeLocation(this.inputPath); 260 | let outputLocation = Directory.normalizeLocation(this.outputPath); 261 | let [...dirs] = outputLocation.dir.split(path.sep); 262 | 263 | let obj = { 264 | input: inputLocation, 265 | output: outputLocation, 266 | size: content.length, 267 | benchmarks: getBenchmarks(this.inputPath, this.outputPath), 268 | }; 269 | 270 | let target = results; 271 | for(let dir of dirs) { 272 | if(!target[dir]) { 273 | target[dir] = {}; 274 | } 275 | target = target[dir]; 276 | } 277 | target[`${SPECIAL_FILE_KEY}${outputLocation.filename}`] = obj; 278 | }); 279 | } 280 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Zach Leatherman @zachleat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

eleventy Logo

2 | 3 | # eleventy-plugin-directory-output 4 | 5 | Group and sort [Eleventy](https://github.com/11ty/eleventy)’s verbose output by directory (and show file size with benchmarks). 6 | 7 | Sample output from `eleventy-base-blog`: 8 | 9 | ``` 10 | > eleventy-base-blog@6.0.0 build 11 | > eleventy 12 | 13 | ↘ _site/ -- -- -- 14 | → about/index.html about/index.md 1.8kB 2.7ms 15 | ↘ feed/ -- -- -- 16 | • .htaccess feed/htaccess.njk 0.1kB 0.2ms 17 | • feed.json feed/json.njk 106.8kB 17.3ms 18 | • feed.xml feed/feed.njk 109.8kB 9.8ms 19 | → page-list/index.html page-list.njk 3.2kB 1.1ms 20 | ↘ posts/ -- -- -- 21 | → firstpost/index.html posts/firstpost.md 3.5kB 1.0ms 22 | → fourthpost/index.html posts/fourthpost.md 101.0kB 27.2ms 23 | → secondpost/index.html posts/secondpost.md 3.2kB 5.6ms 24 | → thirdpost/index.html posts/thirdpost.md 4.5kB 7.5ms 25 | • index.html archive.njk 3.0kB 13.7ms 26 | ↘ tags/ -- -- -- 27 | → another-tag/index.html tags.njk 2.1kB 0.9ms 28 | → number-2/index.html tags.njk 2.1kB 0.4ms 29 | → posts-with-two-tags/index.html tags.njk 2.3kB 0.2ms 30 | → second-tag/index.html tags.njk 2.5kB 0.5ms 31 | • index.html tags-list.njk 2.0kB 0.4ms 32 | • 404.html 404.md 1.9kB 0.4ms 33 | • index.html index.njk 2.8kB 1.7ms 34 | • sitemap.xml sitemap.xml.njk 1.4kB 1.3ms 35 | [11ty] Copied 3 files / Wrote 18 files in 0.16 seconds (8.9ms each, v1.0.1) 36 | ``` 37 | 38 | ## [The full `eleventy-plugin-directory-output` documentation is on 11ty.dev](https://www.11ty.dev/docs/plugins/directory-output/). 39 | 40 | * _This is a plugin for the [Eleventy static site generator](https://www.11ty.dev/)._ 41 | * Find more [Eleventy plugins](https://www.11ty.dev/docs/plugins/). 42 | * Please star [Eleventy on GitHub](https://github.com/11ty/eleventy/), follow [@eleven_ty](https://twitter.com/eleven_ty) on Twitter, and support [11ty on Open Collective](https://opencollective.com/11ty) 43 | 44 | [![npm Version](https://img.shields.io/npm/v/@11ty/eleventy-plugin-directory-output.svg?style=for-the-badge)](https://www.npmjs.com/package/@11ty/eleventy-plugin-directory-output) [![GitHub issues](https://img.shields.io/github/issues/11ty/eleventy-plugin-directory-output.svg?style=for-the-badge)](https://github.com/11ty/eleventy-plugin-directory-output/issues) 45 | 46 | ## Installation 47 | 48 | ``` 49 | npm install @11ty/eleventy-plugin-directory-output 50 | ``` 51 | 52 | _[The full `eleventy-plugin-directory-output` documentation is on 11ty.dev](https://www.11ty.dev/docs/plugins/directory-output/)._ 53 | 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@11ty/eleventy-plugin-directory-output", 3 | "version": "1.0.1", 4 | "description": "Group and sort Eleventy’s verbose output by directory (and show file size with benchmarks)", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "main": ".eleventy.js", 9 | "scripts": {}, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/11ty/eleventy-plugin-directory-output.git" 13 | }, 14 | "funding": { 15 | "type": "opencollective", 16 | "url": "https://opencollective.com/11ty" 17 | }, 18 | "keywords": [ 19 | "eleventy", 20 | "eleventy-plugin" 21 | ], 22 | "author": { 23 | "name": "Zach Leatherman", 24 | "email": "zachleatherman@gmail.com", 25 | "url": "https://zachleat.com/" 26 | }, 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/11ty/eleventy-plugin-directory-output/issues" 30 | }, 31 | "homepage": "https://www.11ty.dev/docs/plugins/directory-output/", 32 | "11ty": { 33 | "compatibility": ">= 1.0.0" 34 | }, 35 | "dependencies": { 36 | "kleur": "^4.1.4", 37 | "strip-color": "^0.1.0" 38 | } 39 | } 40 | --------------------------------------------------------------------------------