├── .gitignore
├── LICENSE
├── package.json
├── README.md
└── .eleventy.js
/.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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@11ty/eleventy-plugin-directory-output",
3 | "version": "1.0.2",
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

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 | [](https://www.npmjs.com/package/@11ty/eleventy-plugin-directory-output) [](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 |
--------------------------------------------------------------------------------
/.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 keysV1 = {
234 | render: `> Render > ${outputPath}`,
235 | compile: `> Compile > ${inputPath}`,
236 | };
237 |
238 | let keysV2 = {
239 | render: `> Render > ${inputPath}`,
240 | compile: `> Compile > ${inputPath}`,
241 | };
242 |
243 | let keysV3 = {
244 | render: `> Render Content > ${inputPath}`,
245 | compile: `> Compile Content > ${inputPath}`,
246 | };
247 |
248 | if(eleventyConfig.benchmarkManager) {
249 | let benchmarkGroup = eleventyConfig.benchmarkManager.get("Aggregate");
250 |
251 | let keys;
252 | if("has" in benchmarkGroup && benchmarkGroup.has(keysV3.render)) {
253 | keys = keysV3;
254 | } else if("has" in benchmarkGroup && benchmarkGroup.has(keysV2.render)) {
255 | keys = keysV2;
256 | } else if("has" in benchmarkGroup && benchmarkGroup.has(keysV1.render)) {
257 | keys = keysV1;
258 | }
259 |
260 | if(keys && benchmarkGroup.has(keys.compile)) {
261 | let b1 = benchmarkGroup.get(keys.render);
262 | benchmarks.render = b1.getTotal();
263 | }
264 |
265 | if(keys && benchmarkGroup.has(keys.compile)) {
266 | let b2 = benchmarkGroup.get(keys.compile);
267 | benchmarks.compile = b2.getTotal();
268 | }
269 | }
270 |
271 | return benchmarks;
272 | }
273 |
274 | eleventyConfig.addLinter("directory-output", function(content) {
275 | if(this.outputPath === false || typeof content !== "string") {
276 | return;
277 | }
278 | let inputLocation = Directory.normalizeLocation(this.inputPath);
279 | let outputLocation = Directory.normalizeLocation(this.outputPath);
280 | let [...dirs] = outputLocation.dir.split(path.sep);
281 |
282 | let obj = {
283 | input: inputLocation,
284 | output: outputLocation,
285 | size: content.length,
286 | benchmarks: getBenchmarks(this.inputPath, this.outputPath),
287 | };
288 |
289 | let target = results;
290 | for(let dir of dirs) {
291 | if(!target[dir]) {
292 | target[dir] = {};
293 | }
294 | target = target[dir];
295 | }
296 | target[`${SPECIAL_FILE_KEY}${outputLocation.filename}`] = obj;
297 | });
298 | }
299 |
--------------------------------------------------------------------------------