@bamboocss/reporter 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,16 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Segun Adebayo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
+ documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8
+ persons to whom the Software is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11
+ Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/dist/index.js ADDED
@@ -0,0 +1,485 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ Reporter: () => Reporter,
34
+ formatRecipeReport: () => formatRecipeReport,
35
+ formatTokenReport: () => formatTokenReport
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // ../../node_modules/.pnpm/markdown-table@3.0.4/node_modules/markdown-table/index.js
40
+ function defaultStringLength(value) {
41
+ return value.length;
42
+ }
43
+ function markdownTable(table2, options) {
44
+ const settings = options || {};
45
+ const align = (settings.align || []).concat();
46
+ const stringLength = settings.stringLength || defaultStringLength;
47
+ const alignments = [];
48
+ const cellMatrix = [];
49
+ const sizeMatrix = [];
50
+ const longestCellByColumn = [];
51
+ let mostCellsPerRow = 0;
52
+ let rowIndex = -1;
53
+ while (++rowIndex < table2.length) {
54
+ const row2 = [];
55
+ const sizes2 = [];
56
+ let columnIndex2 = -1;
57
+ if (table2[rowIndex].length > mostCellsPerRow) {
58
+ mostCellsPerRow = table2[rowIndex].length;
59
+ }
60
+ while (++columnIndex2 < table2[rowIndex].length) {
61
+ const cell = serialize(table2[rowIndex][columnIndex2]);
62
+ if (settings.alignDelimiters !== false) {
63
+ const size = stringLength(cell);
64
+ sizes2[columnIndex2] = size;
65
+ if (longestCellByColumn[columnIndex2] === void 0 || size > longestCellByColumn[columnIndex2]) {
66
+ longestCellByColumn[columnIndex2] = size;
67
+ }
68
+ }
69
+ row2.push(cell);
70
+ }
71
+ cellMatrix[rowIndex] = row2;
72
+ sizeMatrix[rowIndex] = sizes2;
73
+ }
74
+ let columnIndex = -1;
75
+ if (typeof align === "object" && "length" in align) {
76
+ while (++columnIndex < mostCellsPerRow) {
77
+ alignments[columnIndex] = toAlignment(align[columnIndex]);
78
+ }
79
+ } else {
80
+ const code = toAlignment(align);
81
+ while (++columnIndex < mostCellsPerRow) {
82
+ alignments[columnIndex] = code;
83
+ }
84
+ }
85
+ columnIndex = -1;
86
+ const row = [];
87
+ const sizes = [];
88
+ while (++columnIndex < mostCellsPerRow) {
89
+ const code = alignments[columnIndex];
90
+ let before = "";
91
+ let after = "";
92
+ if (code === 99) {
93
+ before = ":";
94
+ after = ":";
95
+ } else if (code === 108) {
96
+ before = ":";
97
+ } else if (code === 114) {
98
+ after = ":";
99
+ }
100
+ let size = settings.alignDelimiters === false ? 1 : Math.max(
101
+ 1,
102
+ longestCellByColumn[columnIndex] - before.length - after.length
103
+ );
104
+ const cell = before + "-".repeat(size) + after;
105
+ if (settings.alignDelimiters !== false) {
106
+ size = before.length + size + after.length;
107
+ if (size > longestCellByColumn[columnIndex]) {
108
+ longestCellByColumn[columnIndex] = size;
109
+ }
110
+ sizes[columnIndex] = size;
111
+ }
112
+ row[columnIndex] = cell;
113
+ }
114
+ cellMatrix.splice(1, 0, row);
115
+ sizeMatrix.splice(1, 0, sizes);
116
+ rowIndex = -1;
117
+ const lines = [];
118
+ while (++rowIndex < cellMatrix.length) {
119
+ const row2 = cellMatrix[rowIndex];
120
+ const sizes2 = sizeMatrix[rowIndex];
121
+ columnIndex = -1;
122
+ const line = [];
123
+ while (++columnIndex < mostCellsPerRow) {
124
+ const cell = row2[columnIndex] || "";
125
+ let before = "";
126
+ let after = "";
127
+ if (settings.alignDelimiters !== false) {
128
+ const size = longestCellByColumn[columnIndex] - (sizes2[columnIndex] || 0);
129
+ const code = alignments[columnIndex];
130
+ if (code === 114) {
131
+ before = " ".repeat(size);
132
+ } else if (code === 99) {
133
+ if (size % 2) {
134
+ before = " ".repeat(size / 2 + 0.5);
135
+ after = " ".repeat(size / 2 - 0.5);
136
+ } else {
137
+ before = " ".repeat(size / 2);
138
+ after = before;
139
+ }
140
+ } else {
141
+ after = " ".repeat(size);
142
+ }
143
+ }
144
+ if (settings.delimiterStart !== false && !columnIndex) {
145
+ line.push("|");
146
+ }
147
+ if (settings.padding !== false && // Don’t add the opening space if we’re not aligning and the cell is
148
+ // empty: there will be a closing space.
149
+ !(settings.alignDelimiters === false && cell === "") && (settings.delimiterStart !== false || columnIndex)) {
150
+ line.push(" ");
151
+ }
152
+ if (settings.alignDelimiters !== false) {
153
+ line.push(before);
154
+ }
155
+ line.push(cell);
156
+ if (settings.alignDelimiters !== false) {
157
+ line.push(after);
158
+ }
159
+ if (settings.padding !== false) {
160
+ line.push(" ");
161
+ }
162
+ if (settings.delimiterEnd !== false || columnIndex !== mostCellsPerRow - 1) {
163
+ line.push("|");
164
+ }
165
+ }
166
+ lines.push(
167
+ settings.delimiterEnd === false ? line.join("").replace(/ +$/, "") : line.join("")
168
+ );
169
+ }
170
+ return lines.join("\n");
171
+ }
172
+ function serialize(value) {
173
+ return value === null || value === void 0 ? "" : String(value);
174
+ }
175
+ function toAlignment(value) {
176
+ const code = typeof value === "string" ? value.codePointAt(0) : 0;
177
+ return code === 67 || code === 99 ? 99 : code === 76 || code === 108 ? 108 : code === 82 || code === 114 ? 114 : 0;
178
+ }
179
+
180
+ // src/report-format.ts
181
+ var import_table = require("table");
182
+ var import_wordwrapjs = __toESM(require("wordwrapjs"));
183
+ var plural = (count, singular) => {
184
+ const pr = new Intl.PluralRules("en-US").select(count);
185
+ const plural2 = pr === "one" || count === 0 ? singular : `${singular}s`;
186
+ return `${count} ${plural2}`;
187
+ };
188
+ var createWrapFn = (enabled) => (str) => enabled ? import_wordwrapjs.default.wrap(str, { width: 20 }) : str;
189
+ function formatTokenReport(result, format) {
190
+ const headers = ["Token", "Usage %", "Most used", "Hardcoded", "Found in"];
191
+ function getFormatted(entry, wrap) {
192
+ const wrapFn = createWrapFn(wrap);
193
+ return [
194
+ `${entry.category} (${plural(entry.count, "token")})`,
195
+ `${entry.percentUsed}% (${plural(entry.usedCount, "token")})`,
196
+ wrapFn(entry.mostUsedNames.join(", ")),
197
+ entry.hardcoded.toString(),
198
+ `${plural(entry.usedInXFiles, "file")}`
199
+ ];
200
+ }
201
+ switch (format) {
202
+ case "json":
203
+ return JSON.stringify(result, null, 2);
204
+ case "markdown": {
205
+ return markdownTable([headers, ...result.map((entry) => getFormatted(entry, true))]);
206
+ }
207
+ case "csv": {
208
+ return [headers.join(","), ...result.map((entry) => getFormatted(entry, false).join(","))].join("\n");
209
+ }
210
+ case "table": {
211
+ return (0, import_table.table)([headers, ...result.map((entry) => getFormatted(entry, true))]);
212
+ }
213
+ case "text":
214
+ default: {
215
+ const formatted = result.map((entry) => getFormatted(entry, false));
216
+ return headers.map((header, index) => `${header}: ${formatted[index]}`).join("\n");
217
+ }
218
+ }
219
+ }
220
+ function formatRecipeReport(result, format) {
221
+ function getFormatted(entry, wrap) {
222
+ const wrapFn = createWrapFn(wrap);
223
+ return [
224
+ `${entry.recipeName} (${plural(entry.variantCount, "variant")})`,
225
+ `${plural(entry.possibleCombinations.length, "value")}`,
226
+ `${entry.percentUsed}% (${plural(entry.usedCombinations, "value")})`,
227
+ wrapFn(entry.mostUsedCombinations.join(", ")),
228
+ `${plural(entry.usedInXFiles, "file")}`,
229
+ `jsx: ${entry.jsxPercentUsed}%
230
+ fn: ${entry.fnPercentUsed}%`
231
+ ];
232
+ }
233
+ const headers = ["Recipe", "Variant values", "Usage %", "Most used", "Found in", "Used as"];
234
+ switch (format) {
235
+ case "json": {
236
+ return JSON.stringify(result, null, 2);
237
+ }
238
+ case "markdown": {
239
+ return (0, import_table.table)([headers, ...result.map((entry) => getFormatted(entry, true))]);
240
+ }
241
+ case "csv": {
242
+ return [headers.join(","), ...result.map((entry) => getFormatted(entry, false).join(","))].join("\n");
243
+ }
244
+ case "table": {
245
+ return (0, import_table.table)([headers, ...result.map((entry) => getFormatted(entry, true))]);
246
+ }
247
+ case "text":
248
+ default: {
249
+ const formatted = result.map((entry) => getFormatted(entry, false));
250
+ return headers.map((header, index) => `${header}: ${formatted[index]}`).join("\n");
251
+ }
252
+ }
253
+ }
254
+
255
+ // src/reporter.ts
256
+ var import_logger = require("@bamboocss/logger");
257
+
258
+ // package.json
259
+ var version = "1.11.1";
260
+
261
+ // src/reporter-recipe.ts
262
+ function analyzeRecipes(ctx, result) {
263
+ const recipesReportItems = Array.from(result.componentByIndex.values()).filter(
264
+ (reportItem) => reportItem.reportItemType === "recipe" || reportItem.reportItemType === "jsx-recipe"
265
+ );
266
+ const recipeReportMap = /* @__PURE__ */ new Map();
267
+ recipesReportItems.forEach((reportItem) => {
268
+ const recipeOrComponentName = reportItem.componentName;
269
+ const recipe = ctx.recipes.details.find(
270
+ (node) => node.match.test(recipeOrComponentName) || node.baseName === recipeOrComponentName
271
+ );
272
+ if (!recipe) return;
273
+ const recipeName = recipe?.baseName;
274
+ if (!recipeReportMap.has(recipeName)) {
275
+ recipeReportMap.set(recipeName, /* @__PURE__ */ new Set());
276
+ }
277
+ recipeReportMap.get(recipeName).add(reportItem);
278
+ });
279
+ const reportMap = Array.from(recipeReportMap.entries());
280
+ const normalizedReportMap = reportMap.map(
281
+ ([recipeName, reportItems]) => [recipeName, Array.from(reportItems)]
282
+ );
283
+ return normalizedReportMap.map(([recipeName, reportItems]) => {
284
+ const usedCombinations = reportItems.map(
285
+ (component) => component.contains.map((id) => {
286
+ const reportItem = result.propByIndex.get(id);
287
+ const recipe2 = ctx.recipes.getRecipe(recipeName);
288
+ if (!recipe2?.variantKeys.includes(reportItem.propName)) return;
289
+ return reportItem.propName + "." + reportItem.value;
290
+ }).filter(Boolean)
291
+ ).flat();
292
+ const distinctUsedCombinations = Array.from(new Set(usedCombinations)).sort();
293
+ const usedCount = reportItems.length;
294
+ const recipe = ctx.recipes.getRecipe(recipeName);
295
+ const variantMap = recipe.variantKeyMap ?? {};
296
+ const possibleCombinations = Object.keys(variantMap).reduce((acc, variantName) => {
297
+ return acc.concat(variantMap[variantName].map((value) => `${variantName}.${value}`));
298
+ }, []);
299
+ const variantCount = recipe.variantKeys.length;
300
+ const percentUsed = Math.ceil(distinctUsedCombinations.length / (possibleCombinations.length || 1) * 1e4) / 100;
301
+ const jsxUsage = reportItems.filter((component) => component.reportItemType === "jsx-recipe");
302
+ const fnUsage = reportItems.filter((component) => component.reportItemType === "recipe");
303
+ const jsxPercentUsed = Math.ceil(jsxUsage.length / (reportItems.length || 1) * 100);
304
+ const fnPercentUsed = Math.ceil(fnUsage.length / (reportItems.length || 1) * 100);
305
+ const usedInXFiles = new Set(reportItems.flatMap((component) => component.filepath)).size;
306
+ return {
307
+ recipeName,
308
+ usedInXFiles,
309
+ usedCount,
310
+ variantCount,
311
+ possibleCombinations,
312
+ usedCombinations: distinctUsedCombinations.length,
313
+ percentUsed,
314
+ jsxPercentUsed,
315
+ fnPercentUsed,
316
+ unusedCombinations: possibleCombinations.length - distinctUsedCombinations.length,
317
+ mostUsedCombinations: distinctUsedCombinations.slice(0, 5)
318
+ };
319
+ }).sort((a, b) => b.percentUsed - a.percentUsed);
320
+ }
321
+
322
+ // src/reporter-token.ts
323
+ var import_shared = require("@bamboocss/shared");
324
+ var formatter = new Intl.NumberFormat("en-US", {
325
+ maximumFractionDigits: 2,
326
+ minimumFractionDigits: 2
327
+ });
328
+ var getPercent = (used, total) => {
329
+ return Number(formatter.format(used / (total || 1) * 100));
330
+ };
331
+ function analyzeTokens(ctx, result) {
332
+ const categoryMap = result.derived.globalMaps.byTokenType;
333
+ const categoryEntries = Array.from(categoryMap.entries());
334
+ const usageMap = /* @__PURE__ */ new Map();
335
+ const totalMap = /* @__PURE__ */ new Map();
336
+ categoryEntries.forEach(([category, categoryIds]) => {
337
+ const usage = usageMap.get(category) || usageMap.set(category, []).get(category);
338
+ categoryIds.forEach((id) => {
339
+ const item = result.propByIndex.get(id);
340
+ if (item?.value == null) return;
341
+ const type = item.isKnownValue ? "token" : "nonToken";
342
+ const value = item.value.toString();
343
+ const filePath = item.filepath;
344
+ const loc = item.range ? {
345
+ line: item.range.startLineNumber,
346
+ column: item.range.startColumn
347
+ } : null;
348
+ usage.push({ category, value, filePath, loc, type });
349
+ });
350
+ const totalTokens = ctx.tokens.view.categoryMap.get(category)?.size ?? 0;
351
+ totalMap.set(category, totalTokens);
352
+ });
353
+ const usageEntries = Array.from(usageMap.entries());
354
+ const percentMap = usageEntries.reduce((map, [category, usage]) => {
355
+ const total = totalMap.get(category) ?? 0;
356
+ const tokens = usage.reduce((acc, item) => {
357
+ return item.type === "token" ? acc.add(item.value) : acc;
358
+ }, /* @__PURE__ */ new Set());
359
+ const percent = getPercent(tokens.size, total);
360
+ return map.set(category, {
361
+ total,
362
+ used: tokens.size,
363
+ unused: total - tokens.size,
364
+ percent
365
+ });
366
+ }, /* @__PURE__ */ new Map());
367
+ const tokenNameMap = usageEntries.reduce((map, [category, usage]) => {
368
+ const existing = map.get(category) ?? [];
369
+ usage.forEach(({ value, type }) => {
370
+ if (type === "token") existing.push(value);
371
+ });
372
+ const sorted = (0, import_shared.uniq)(existing).sort(
373
+ (a, b) => (result.derived.globalMaps.byTokenName.get(b)?.size ?? 0) - (result.derived.globalMaps.byTokenName.get(a)?.size ?? 0)
374
+ );
375
+ return map.set(category, sorted);
376
+ }, /* @__PURE__ */ new Map());
377
+ const fileUsageMap = usageEntries.reduce((map, [category, usage]) => {
378
+ const existing = map.get(category) ?? /* @__PURE__ */ new Set();
379
+ usage.forEach(({ filePath }) => {
380
+ if (filePath.startsWith("@config")) return;
381
+ existing.add(filePath);
382
+ });
383
+ return map.set(category, existing);
384
+ }, /* @__PURE__ */ new Map());
385
+ const hardcodedTokenMap = usageEntries.reduce((map, [category, usage]) => {
386
+ const items = /* @__PURE__ */ new Set();
387
+ usage.forEach(({ type, value }) => {
388
+ if (type === "nonToken") items.add(value);
389
+ });
390
+ return map.set(category, items.size);
391
+ }, /* @__PURE__ */ new Map());
392
+ return {
393
+ usageMap,
394
+ percentMap,
395
+ tokenNameMap,
396
+ fileUsageMap,
397
+ hardcodedTokenMap,
398
+ totalMap,
399
+ getSummary() {
400
+ const summary = categoryEntries.map(([category]) => {
401
+ const percent = percentMap.get(category);
402
+ return {
403
+ category,
404
+ count: percent?.total ?? 0,
405
+ usedInXFiles: fileUsageMap.get(category)?.size ?? 0,
406
+ usedCount: percent?.used ?? 0,
407
+ percentUsed: percent?.percent ?? 0,
408
+ hardcoded: hardcodedTokenMap.get(category) ?? 0,
409
+ mostUsedNames: tokenNameMap.get(category)?.slice(0, 5) ?? []
410
+ };
411
+ });
412
+ return summary.sort((a, b) => b.percentUsed - a.percentUsed);
413
+ }
414
+ };
415
+ }
416
+
417
+ // src/reporter.ts
418
+ var Reporter = class {
419
+ constructor(ctx, options) {
420
+ this.ctx = ctx;
421
+ this.options = options;
422
+ }
423
+ #parserResults = /* @__PURE__ */ new Map();
424
+ #extractTimes = /* @__PURE__ */ new Map();
425
+ #sheet;
426
+ #report;
427
+ setup = () => {
428
+ this.#sheet = this.ctx.createSheet();
429
+ this.ctx.appendLayerParams(this.#sheet);
430
+ this.ctx.appendBaselineCss(this.#sheet);
431
+ this.parseFiles();
432
+ this.ctx.appendParserCss(this.#sheet);
433
+ };
434
+ get report() {
435
+ return this.#report;
436
+ }
437
+ parseFiles = () => {
438
+ const { getFiles } = this.options;
439
+ const files = getFiles();
440
+ import_logger.logger.info("analyze", `Analyzing ${files.length} file(s) for token and recipe usage...`);
441
+ for (const file of files) {
442
+ this.parseFile(file);
443
+ }
444
+ };
445
+ parseFile = (file) => {
446
+ const { project, getRelativePath, onResult } = this.options;
447
+ const { config } = this.ctx;
448
+ const start = performance.now();
449
+ const result = project.parseSourceFile?.(file);
450
+ const extractMs = performance.now() - start;
451
+ const filePath = getRelativePath(config.cwd, file);
452
+ this.#extractTimes.set(filePath, extractMs);
453
+ import_logger.logger.debug("analyze", `Parsed ${file} in ${extractMs}ms`);
454
+ if (result) {
455
+ this.#parserResults.set(filePath, result);
456
+ onResult?.(file, result);
457
+ }
458
+ };
459
+ init = () => {
460
+ const { project } = this.options;
461
+ this.setup();
462
+ const classify = project.classify(this.#parserResults);
463
+ this.#report = {
464
+ schemaVersion: version,
465
+ details: classify.details,
466
+ propByIndex: classify.propById,
467
+ componentByIndex: classify.componentById,
468
+ derived: classify.derived
469
+ };
470
+ };
471
+ getTokenReport = () => {
472
+ const { project } = this.options;
473
+ return analyzeTokens(project.parserOptions, this.#report);
474
+ };
475
+ getRecipeReport = () => {
476
+ const { project } = this.options;
477
+ return analyzeRecipes(project.parserOptions, this.#report);
478
+ };
479
+ };
480
+ // Annotate the CommonJS export names for ESM import in node:
481
+ 0 && (module.exports = {
482
+ Reporter,
483
+ formatRecipeReport,
484
+ formatTokenReport
485
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,446 @@
1
+ // ../../node_modules/.pnpm/markdown-table@3.0.4/node_modules/markdown-table/index.js
2
+ function defaultStringLength(value) {
3
+ return value.length;
4
+ }
5
+ function markdownTable(table2, options) {
6
+ const settings = options || {};
7
+ const align = (settings.align || []).concat();
8
+ const stringLength = settings.stringLength || defaultStringLength;
9
+ const alignments = [];
10
+ const cellMatrix = [];
11
+ const sizeMatrix = [];
12
+ const longestCellByColumn = [];
13
+ let mostCellsPerRow = 0;
14
+ let rowIndex = -1;
15
+ while (++rowIndex < table2.length) {
16
+ const row2 = [];
17
+ const sizes2 = [];
18
+ let columnIndex2 = -1;
19
+ if (table2[rowIndex].length > mostCellsPerRow) {
20
+ mostCellsPerRow = table2[rowIndex].length;
21
+ }
22
+ while (++columnIndex2 < table2[rowIndex].length) {
23
+ const cell = serialize(table2[rowIndex][columnIndex2]);
24
+ if (settings.alignDelimiters !== false) {
25
+ const size = stringLength(cell);
26
+ sizes2[columnIndex2] = size;
27
+ if (longestCellByColumn[columnIndex2] === void 0 || size > longestCellByColumn[columnIndex2]) {
28
+ longestCellByColumn[columnIndex2] = size;
29
+ }
30
+ }
31
+ row2.push(cell);
32
+ }
33
+ cellMatrix[rowIndex] = row2;
34
+ sizeMatrix[rowIndex] = sizes2;
35
+ }
36
+ let columnIndex = -1;
37
+ if (typeof align === "object" && "length" in align) {
38
+ while (++columnIndex < mostCellsPerRow) {
39
+ alignments[columnIndex] = toAlignment(align[columnIndex]);
40
+ }
41
+ } else {
42
+ const code = toAlignment(align);
43
+ while (++columnIndex < mostCellsPerRow) {
44
+ alignments[columnIndex] = code;
45
+ }
46
+ }
47
+ columnIndex = -1;
48
+ const row = [];
49
+ const sizes = [];
50
+ while (++columnIndex < mostCellsPerRow) {
51
+ const code = alignments[columnIndex];
52
+ let before = "";
53
+ let after = "";
54
+ if (code === 99) {
55
+ before = ":";
56
+ after = ":";
57
+ } else if (code === 108) {
58
+ before = ":";
59
+ } else if (code === 114) {
60
+ after = ":";
61
+ }
62
+ let size = settings.alignDelimiters === false ? 1 : Math.max(
63
+ 1,
64
+ longestCellByColumn[columnIndex] - before.length - after.length
65
+ );
66
+ const cell = before + "-".repeat(size) + after;
67
+ if (settings.alignDelimiters !== false) {
68
+ size = before.length + size + after.length;
69
+ if (size > longestCellByColumn[columnIndex]) {
70
+ longestCellByColumn[columnIndex] = size;
71
+ }
72
+ sizes[columnIndex] = size;
73
+ }
74
+ row[columnIndex] = cell;
75
+ }
76
+ cellMatrix.splice(1, 0, row);
77
+ sizeMatrix.splice(1, 0, sizes);
78
+ rowIndex = -1;
79
+ const lines = [];
80
+ while (++rowIndex < cellMatrix.length) {
81
+ const row2 = cellMatrix[rowIndex];
82
+ const sizes2 = sizeMatrix[rowIndex];
83
+ columnIndex = -1;
84
+ const line = [];
85
+ while (++columnIndex < mostCellsPerRow) {
86
+ const cell = row2[columnIndex] || "";
87
+ let before = "";
88
+ let after = "";
89
+ if (settings.alignDelimiters !== false) {
90
+ const size = longestCellByColumn[columnIndex] - (sizes2[columnIndex] || 0);
91
+ const code = alignments[columnIndex];
92
+ if (code === 114) {
93
+ before = " ".repeat(size);
94
+ } else if (code === 99) {
95
+ if (size % 2) {
96
+ before = " ".repeat(size / 2 + 0.5);
97
+ after = " ".repeat(size / 2 - 0.5);
98
+ } else {
99
+ before = " ".repeat(size / 2);
100
+ after = before;
101
+ }
102
+ } else {
103
+ after = " ".repeat(size);
104
+ }
105
+ }
106
+ if (settings.delimiterStart !== false && !columnIndex) {
107
+ line.push("|");
108
+ }
109
+ if (settings.padding !== false && // Don’t add the opening space if we’re not aligning and the cell is
110
+ // empty: there will be a closing space.
111
+ !(settings.alignDelimiters === false && cell === "") && (settings.delimiterStart !== false || columnIndex)) {
112
+ line.push(" ");
113
+ }
114
+ if (settings.alignDelimiters !== false) {
115
+ line.push(before);
116
+ }
117
+ line.push(cell);
118
+ if (settings.alignDelimiters !== false) {
119
+ line.push(after);
120
+ }
121
+ if (settings.padding !== false) {
122
+ line.push(" ");
123
+ }
124
+ if (settings.delimiterEnd !== false || columnIndex !== mostCellsPerRow - 1) {
125
+ line.push("|");
126
+ }
127
+ }
128
+ lines.push(
129
+ settings.delimiterEnd === false ? line.join("").replace(/ +$/, "") : line.join("")
130
+ );
131
+ }
132
+ return lines.join("\n");
133
+ }
134
+ function serialize(value) {
135
+ return value === null || value === void 0 ? "" : String(value);
136
+ }
137
+ function toAlignment(value) {
138
+ const code = typeof value === "string" ? value.codePointAt(0) : 0;
139
+ return code === 67 || code === 99 ? 99 : code === 76 || code === 108 ? 108 : code === 82 || code === 114 ? 114 : 0;
140
+ }
141
+
142
+ // src/report-format.ts
143
+ import { table } from "table";
144
+ import Wordwrap from "wordwrapjs";
145
+ var plural = (count, singular) => {
146
+ const pr = new Intl.PluralRules("en-US").select(count);
147
+ const plural2 = pr === "one" || count === 0 ? singular : `${singular}s`;
148
+ return `${count} ${plural2}`;
149
+ };
150
+ var createWrapFn = (enabled) => (str) => enabled ? Wordwrap.wrap(str, { width: 20 }) : str;
151
+ function formatTokenReport(result, format) {
152
+ const headers = ["Token", "Usage %", "Most used", "Hardcoded", "Found in"];
153
+ function getFormatted(entry, wrap) {
154
+ const wrapFn = createWrapFn(wrap);
155
+ return [
156
+ `${entry.category} (${plural(entry.count, "token")})`,
157
+ `${entry.percentUsed}% (${plural(entry.usedCount, "token")})`,
158
+ wrapFn(entry.mostUsedNames.join(", ")),
159
+ entry.hardcoded.toString(),
160
+ `${plural(entry.usedInXFiles, "file")}`
161
+ ];
162
+ }
163
+ switch (format) {
164
+ case "json":
165
+ return JSON.stringify(result, null, 2);
166
+ case "markdown": {
167
+ return markdownTable([headers, ...result.map((entry) => getFormatted(entry, true))]);
168
+ }
169
+ case "csv": {
170
+ return [headers.join(","), ...result.map((entry) => getFormatted(entry, false).join(","))].join("\n");
171
+ }
172
+ case "table": {
173
+ return table([headers, ...result.map((entry) => getFormatted(entry, true))]);
174
+ }
175
+ case "text":
176
+ default: {
177
+ const formatted = result.map((entry) => getFormatted(entry, false));
178
+ return headers.map((header, index) => `${header}: ${formatted[index]}`).join("\n");
179
+ }
180
+ }
181
+ }
182
+ function formatRecipeReport(result, format) {
183
+ function getFormatted(entry, wrap) {
184
+ const wrapFn = createWrapFn(wrap);
185
+ return [
186
+ `${entry.recipeName} (${plural(entry.variantCount, "variant")})`,
187
+ `${plural(entry.possibleCombinations.length, "value")}`,
188
+ `${entry.percentUsed}% (${plural(entry.usedCombinations, "value")})`,
189
+ wrapFn(entry.mostUsedCombinations.join(", ")),
190
+ `${plural(entry.usedInXFiles, "file")}`,
191
+ `jsx: ${entry.jsxPercentUsed}%
192
+ fn: ${entry.fnPercentUsed}%`
193
+ ];
194
+ }
195
+ const headers = ["Recipe", "Variant values", "Usage %", "Most used", "Found in", "Used as"];
196
+ switch (format) {
197
+ case "json": {
198
+ return JSON.stringify(result, null, 2);
199
+ }
200
+ case "markdown": {
201
+ return table([headers, ...result.map((entry) => getFormatted(entry, true))]);
202
+ }
203
+ case "csv": {
204
+ return [headers.join(","), ...result.map((entry) => getFormatted(entry, false).join(","))].join("\n");
205
+ }
206
+ case "table": {
207
+ return table([headers, ...result.map((entry) => getFormatted(entry, true))]);
208
+ }
209
+ case "text":
210
+ default: {
211
+ const formatted = result.map((entry) => getFormatted(entry, false));
212
+ return headers.map((header, index) => `${header}: ${formatted[index]}`).join("\n");
213
+ }
214
+ }
215
+ }
216
+
217
+ // src/reporter.ts
218
+ import { logger } from "@bamboocss/logger";
219
+
220
+ // package.json
221
+ var version = "1.11.1";
222
+
223
+ // src/reporter-recipe.ts
224
+ function analyzeRecipes(ctx, result) {
225
+ const recipesReportItems = Array.from(result.componentByIndex.values()).filter(
226
+ (reportItem) => reportItem.reportItemType === "recipe" || reportItem.reportItemType === "jsx-recipe"
227
+ );
228
+ const recipeReportMap = /* @__PURE__ */ new Map();
229
+ recipesReportItems.forEach((reportItem) => {
230
+ const recipeOrComponentName = reportItem.componentName;
231
+ const recipe = ctx.recipes.details.find(
232
+ (node) => node.match.test(recipeOrComponentName) || node.baseName === recipeOrComponentName
233
+ );
234
+ if (!recipe) return;
235
+ const recipeName = recipe?.baseName;
236
+ if (!recipeReportMap.has(recipeName)) {
237
+ recipeReportMap.set(recipeName, /* @__PURE__ */ new Set());
238
+ }
239
+ recipeReportMap.get(recipeName).add(reportItem);
240
+ });
241
+ const reportMap = Array.from(recipeReportMap.entries());
242
+ const normalizedReportMap = reportMap.map(
243
+ ([recipeName, reportItems]) => [recipeName, Array.from(reportItems)]
244
+ );
245
+ return normalizedReportMap.map(([recipeName, reportItems]) => {
246
+ const usedCombinations = reportItems.map(
247
+ (component) => component.contains.map((id) => {
248
+ const reportItem = result.propByIndex.get(id);
249
+ const recipe2 = ctx.recipes.getRecipe(recipeName);
250
+ if (!recipe2?.variantKeys.includes(reportItem.propName)) return;
251
+ return reportItem.propName + "." + reportItem.value;
252
+ }).filter(Boolean)
253
+ ).flat();
254
+ const distinctUsedCombinations = Array.from(new Set(usedCombinations)).sort();
255
+ const usedCount = reportItems.length;
256
+ const recipe = ctx.recipes.getRecipe(recipeName);
257
+ const variantMap = recipe.variantKeyMap ?? {};
258
+ const possibleCombinations = Object.keys(variantMap).reduce((acc, variantName) => {
259
+ return acc.concat(variantMap[variantName].map((value) => `${variantName}.${value}`));
260
+ }, []);
261
+ const variantCount = recipe.variantKeys.length;
262
+ const percentUsed = Math.ceil(distinctUsedCombinations.length / (possibleCombinations.length || 1) * 1e4) / 100;
263
+ const jsxUsage = reportItems.filter((component) => component.reportItemType === "jsx-recipe");
264
+ const fnUsage = reportItems.filter((component) => component.reportItemType === "recipe");
265
+ const jsxPercentUsed = Math.ceil(jsxUsage.length / (reportItems.length || 1) * 100);
266
+ const fnPercentUsed = Math.ceil(fnUsage.length / (reportItems.length || 1) * 100);
267
+ const usedInXFiles = new Set(reportItems.flatMap((component) => component.filepath)).size;
268
+ return {
269
+ recipeName,
270
+ usedInXFiles,
271
+ usedCount,
272
+ variantCount,
273
+ possibleCombinations,
274
+ usedCombinations: distinctUsedCombinations.length,
275
+ percentUsed,
276
+ jsxPercentUsed,
277
+ fnPercentUsed,
278
+ unusedCombinations: possibleCombinations.length - distinctUsedCombinations.length,
279
+ mostUsedCombinations: distinctUsedCombinations.slice(0, 5)
280
+ };
281
+ }).sort((a, b) => b.percentUsed - a.percentUsed);
282
+ }
283
+
284
+ // src/reporter-token.ts
285
+ import { uniq } from "@bamboocss/shared";
286
+ var formatter = new Intl.NumberFormat("en-US", {
287
+ maximumFractionDigits: 2,
288
+ minimumFractionDigits: 2
289
+ });
290
+ var getPercent = (used, total) => {
291
+ return Number(formatter.format(used / (total || 1) * 100));
292
+ };
293
+ function analyzeTokens(ctx, result) {
294
+ const categoryMap = result.derived.globalMaps.byTokenType;
295
+ const categoryEntries = Array.from(categoryMap.entries());
296
+ const usageMap = /* @__PURE__ */ new Map();
297
+ const totalMap = /* @__PURE__ */ new Map();
298
+ categoryEntries.forEach(([category, categoryIds]) => {
299
+ const usage = usageMap.get(category) || usageMap.set(category, []).get(category);
300
+ categoryIds.forEach((id) => {
301
+ const item = result.propByIndex.get(id);
302
+ if (item?.value == null) return;
303
+ const type = item.isKnownValue ? "token" : "nonToken";
304
+ const value = item.value.toString();
305
+ const filePath = item.filepath;
306
+ const loc = item.range ? {
307
+ line: item.range.startLineNumber,
308
+ column: item.range.startColumn
309
+ } : null;
310
+ usage.push({ category, value, filePath, loc, type });
311
+ });
312
+ const totalTokens = ctx.tokens.view.categoryMap.get(category)?.size ?? 0;
313
+ totalMap.set(category, totalTokens);
314
+ });
315
+ const usageEntries = Array.from(usageMap.entries());
316
+ const percentMap = usageEntries.reduce((map, [category, usage]) => {
317
+ const total = totalMap.get(category) ?? 0;
318
+ const tokens = usage.reduce((acc, item) => {
319
+ return item.type === "token" ? acc.add(item.value) : acc;
320
+ }, /* @__PURE__ */ new Set());
321
+ const percent = getPercent(tokens.size, total);
322
+ return map.set(category, {
323
+ total,
324
+ used: tokens.size,
325
+ unused: total - tokens.size,
326
+ percent
327
+ });
328
+ }, /* @__PURE__ */ new Map());
329
+ const tokenNameMap = usageEntries.reduce((map, [category, usage]) => {
330
+ const existing = map.get(category) ?? [];
331
+ usage.forEach(({ value, type }) => {
332
+ if (type === "token") existing.push(value);
333
+ });
334
+ const sorted = uniq(existing).sort(
335
+ (a, b) => (result.derived.globalMaps.byTokenName.get(b)?.size ?? 0) - (result.derived.globalMaps.byTokenName.get(a)?.size ?? 0)
336
+ );
337
+ return map.set(category, sorted);
338
+ }, /* @__PURE__ */ new Map());
339
+ const fileUsageMap = usageEntries.reduce((map, [category, usage]) => {
340
+ const existing = map.get(category) ?? /* @__PURE__ */ new Set();
341
+ usage.forEach(({ filePath }) => {
342
+ if (filePath.startsWith("@config")) return;
343
+ existing.add(filePath);
344
+ });
345
+ return map.set(category, existing);
346
+ }, /* @__PURE__ */ new Map());
347
+ const hardcodedTokenMap = usageEntries.reduce((map, [category, usage]) => {
348
+ const items = /* @__PURE__ */ new Set();
349
+ usage.forEach(({ type, value }) => {
350
+ if (type === "nonToken") items.add(value);
351
+ });
352
+ return map.set(category, items.size);
353
+ }, /* @__PURE__ */ new Map());
354
+ return {
355
+ usageMap,
356
+ percentMap,
357
+ tokenNameMap,
358
+ fileUsageMap,
359
+ hardcodedTokenMap,
360
+ totalMap,
361
+ getSummary() {
362
+ const summary = categoryEntries.map(([category]) => {
363
+ const percent = percentMap.get(category);
364
+ return {
365
+ category,
366
+ count: percent?.total ?? 0,
367
+ usedInXFiles: fileUsageMap.get(category)?.size ?? 0,
368
+ usedCount: percent?.used ?? 0,
369
+ percentUsed: percent?.percent ?? 0,
370
+ hardcoded: hardcodedTokenMap.get(category) ?? 0,
371
+ mostUsedNames: tokenNameMap.get(category)?.slice(0, 5) ?? []
372
+ };
373
+ });
374
+ return summary.sort((a, b) => b.percentUsed - a.percentUsed);
375
+ }
376
+ };
377
+ }
378
+
379
+ // src/reporter.ts
380
+ var Reporter = class {
381
+ constructor(ctx, options) {
382
+ this.ctx = ctx;
383
+ this.options = options;
384
+ }
385
+ #parserResults = /* @__PURE__ */ new Map();
386
+ #extractTimes = /* @__PURE__ */ new Map();
387
+ #sheet;
388
+ #report;
389
+ setup = () => {
390
+ this.#sheet = this.ctx.createSheet();
391
+ this.ctx.appendLayerParams(this.#sheet);
392
+ this.ctx.appendBaselineCss(this.#sheet);
393
+ this.parseFiles();
394
+ this.ctx.appendParserCss(this.#sheet);
395
+ };
396
+ get report() {
397
+ return this.#report;
398
+ }
399
+ parseFiles = () => {
400
+ const { getFiles } = this.options;
401
+ const files = getFiles();
402
+ logger.info("analyze", `Analyzing ${files.length} file(s) for token and recipe usage...`);
403
+ for (const file of files) {
404
+ this.parseFile(file);
405
+ }
406
+ };
407
+ parseFile = (file) => {
408
+ const { project, getRelativePath, onResult } = this.options;
409
+ const { config } = this.ctx;
410
+ const start = performance.now();
411
+ const result = project.parseSourceFile?.(file);
412
+ const extractMs = performance.now() - start;
413
+ const filePath = getRelativePath(config.cwd, file);
414
+ this.#extractTimes.set(filePath, extractMs);
415
+ logger.debug("analyze", `Parsed ${file} in ${extractMs}ms`);
416
+ if (result) {
417
+ this.#parserResults.set(filePath, result);
418
+ onResult?.(file, result);
419
+ }
420
+ };
421
+ init = () => {
422
+ const { project } = this.options;
423
+ this.setup();
424
+ const classify = project.classify(this.#parserResults);
425
+ this.#report = {
426
+ schemaVersion: version,
427
+ details: classify.details,
428
+ propByIndex: classify.propById,
429
+ componentByIndex: classify.componentById,
430
+ derived: classify.derived
431
+ };
432
+ };
433
+ getTokenReport = () => {
434
+ const { project } = this.options;
435
+ return analyzeTokens(project.parserOptions, this.#report);
436
+ };
437
+ getRecipeReport = () => {
438
+ const { project } = this.options;
439
+ return analyzeRecipes(project.parserOptions, this.#report);
440
+ };
441
+ };
442
+ export {
443
+ Reporter,
444
+ formatRecipeReport,
445
+ formatTokenReport
446
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@bamboocss/reporter",
3
+ "version": "1.11.1",
4
+ "description": "Track and report usage of tokens and recipes",
5
+ "homepage": "https://bamboo-css.com",
6
+ "license": "MIT",
7
+ "author": "Segun Adebayo <joseshegs@gmail.com>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/chakra-ui/bamboo.git",
11
+ "directory": "packages/reporter"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "sideEffects": false,
17
+ "main": "dist/index.js",
18
+ "module": "dist/index.mjs",
19
+ "types": "dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "source": "./src/index.ts",
23
+ "types": "./dist/index.d.ts",
24
+ "require": "./dist/index.js",
25
+ "import": {
26
+ "types": "./dist/index.d.mts",
27
+ "default": "./dist/index.mjs"
28
+ }
29
+ },
30
+ "./package.json": "./package.json"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "dependencies": {
36
+ "table": "6.9.0",
37
+ "wordwrapjs": "5.1.1",
38
+ "@bamboocss/core": "1.11.1",
39
+ "@bamboocss/generator": "1.11.1",
40
+ "@bamboocss/logger": "1.11.1",
41
+ "@bamboocss/shared": "1.11.1",
42
+ "@bamboocss/types": "1.11.1"
43
+ },
44
+ "devDependencies": {
45
+ "markdown-table": "3.0.4"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup src/index.ts --format=esm,cjs --dts",
49
+ "build-fast": "tsup src/index.ts --format=esm,cjs --no-dts",
50
+ "dev": "pnpm build-fast --watch"
51
+ }
52
+ }