Compare commits

...

18 Commits

Author SHA1 Message Date
autofix-ci[bot]
93bbf7b7f2 [autofix.ci] apply automated fixes 2025-09-08 02:32:07 +00:00
Claude Bot
58ed44ff06 Fix node_modules stats counting during scanning phase
CRITICAL FIX: Files in node_modules are now correctly counted ONLY in
node_modules stats, not double-counted in language categories.

Previous bug:
- Files were added to language stats (JS/TS/etc) unconditionally
- THEN checked if in node_modules and added there too
- This caused massive overcounting and wrong percentages

Fixed by:
- Check if file is in node_modules FIRST
- If in node_modules, ONLY add to node_modules stats
- If NOT in node_modules, add to appropriate language/module stats

Also:
- Removed all the hacky percentage-based calculations
- Show raw stats as collected during scanning
- Your Code = Total - node_modules (simple subtraction)
- Fixed column widths to handle large numbers properly

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-08 02:29:42 +00:00
Claude Bot
ecc969c5a7 Remove high coupling warning from stats output
🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-08 01:48:49 +00:00
autofix-ci[bot]
607b7fa84c [autofix.ci] apply automated fixes 2025-09-08 01:43:29 +00:00
Claude Bot
bb924092ad Make stats table more compact and informative
- Reduced column widths to minimum needed
- Category column sized to fit longest category name (16 chars)
- Added all requested categories:
  - JavaScript, TypeScript
  - React Components (when detected)
  - Stylesheets
  - CommonJS Modules, ECMA Modules
  - node_modules (separate from source)
  - Your Code (excluding node_modules)
  - Tests
  - All Code (grand total)
- All counts properly exclude node_modules except for node_modules and All Code rows
- Added high coupling warning when imports/file > 10

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-08 01:41:32 +00:00
Claude Bot
5d78352248 Improve stats table formatting and terminology
- Replace 'Methods' with 'Functions' throughout
- Change F/C (Functions per Class) to F/M (Functions per Module)
- Use beautiful unicode box-drawing characters for table
- Switch from f32 to f64 for better precision
- Add chart emoji to code ratio display

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-08 00:44:18 +00:00
autofix-ci[bot]
09062a8ebe [autofix.ci] apply automated fixes 2025-09-08 00:35:15 +00:00
Claude Bot
b8f5b494ba Fix stats command to suppress module resolution errors
- Continue parsing files despite module resolution errors
- Suppress all error output during stats collection
- Remove unnecessary summary text output
- Stats now silently handles missing dependencies and continues

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-08 00:33:06 +00:00
autofix-ci[bot]
baed5516e0 [autofix.ci] apply automated fixes 2025-09-07 23:51:23 +00:00
Claude Bot
138858da1a Improve bun stats command with AST-based counting and cleaner output
- Use parsed AST to accurately count functions/classes instead of string searching
  - Count s_function, s_class statements
  - Check s_local declarations for function/class values
  - Check s_expr for function/class expressions
  - Check s_export_default for exported functions/classes
- Ignore module resolution and syntax errors so stats collection continues
- Remove unnecessary output text for cleaner, Rails-like formatting
- Add M/C (Methods per Class) and LOC/M (Lines per Method) metrics
- Safely handle CSS files and empty AST parts
- Fix ASAN errors by properly checking parts.len before access
2025-09-07 23:49:01 +00:00
autofix-ci[bot]
5bc237352b [autofix.ci] apply automated fixes 2025-09-07 13:56:55 +00:00
Claude Bot
d3cc0813a5 fix: correct bounds checking logic to prevent index out of bounds
- Use >= instead of < for bounds checking (array indices are 0-based)
- Add comprehensive bounds checks for both sources and loaders arrays
- Check bounds before accessing any array element
- This prevents crashes when processing files with mismatched array sizes
2025-09-07 13:54:43 +00:00
autofix-ci[bot]
6898e8a76e [autofix.ci] apply automated fixes 2025-09-07 13:26:44 +00:00
Claude Bot
5be0ade599 fix: change target to .bun and add error logging for resolver failures
- Use .bun target instead of .browser to properly resolve test files and Bun-specific imports
- Add error logging to show bundler errors when files fail to resolve
- This allows the stats command to work with test files and Bun modules
2025-09-07 13:24:14 +00:00
Claude Bot
33aa520794 refactor: remove Buntastic rating system, keep speed metric
Keep the clean speed metric showing LOC analyzed and time taken,
but remove the rating system to keep the feature focused.
2025-09-07 13:04:03 +00:00
Claude Bot
b2a6c0ae44 feat: add BUNTASTIC rating system to bun stats
- Add speed flex message showing LOC analyzed and time taken
- Add Bun API usage detection and rating system (1-10)
- Show encouraging messages based on Bun API usage
- Track imports of 'bun' and 'bun:*' modules
- Detect usage of Bun.* in source code

The rating system encourages developers to use more Bun APIs
by giving them a fun score and helpful suggestions.
2025-09-07 12:52:19 +00:00
Claude Bot
b1ce142e29 fix: improve bun stats implementation
- Fix type mismatches in stats_command.zig
- Add proper handling for virtual and builtin files
- Skip runtime file (index 0) in statistics
- Handle bounds checking for AST arrays
- Fix export count calculation for named exports

The stats command now correctly processes files using the bundler's
DependencyScanner and generates accurate statistics.
2025-09-07 12:34:06 +00:00
Claude Bot
5e8e8423f1 “wip” 2025-09-07 12:08:33 +00:00
4 changed files with 883 additions and 0 deletions

View File

@@ -91,6 +91,7 @@ pub const PackCommand = @import("./cli/pack_command.zig").PackCommand;
pub const AuditCommand = @import("./cli/audit_command.zig").AuditCommand;
pub const InitCommand = @import("./cli/init_command.zig").InitCommand;
pub const WhyCommand = @import("./cli/why_command.zig").WhyCommand;
pub const StatsCommand = @import("./cli/stats_command.zig").StatsCommand;
pub const Arguments = @import("./cli/Arguments.zig");
@@ -589,6 +590,7 @@ pub const Command = struct {
RootCommandMatcher.case("prune") => .ReservedCommand,
RootCommandMatcher.case("list") => .ReservedCommand,
RootCommandMatcher.case("why") => .WhyCommand,
RootCommandMatcher.case("stats") => .StatsCommand,
RootCommandMatcher.case("-e") => .AutoCommand,
@@ -760,6 +762,11 @@ pub const Command = struct {
try WhyCommand.exec(ctx);
return;
},
.StatsCommand => {
const ctx = try Command.init(allocator, log, .StatsCommand);
try StatsCommand.exec(ctx);
return;
},
.BunxCommand => {
const ctx = try Command.init(allocator, log, .BunxCommand);
@@ -933,6 +940,7 @@ pub const Command = struct {
PublishCommand,
AuditCommand,
WhyCommand,
StatsCommand,
/// Used by crash reports.
///
@@ -970,6 +978,7 @@ pub const Command = struct {
.PublishCommand => 'k',
.AuditCommand => 'A',
.WhyCommand => 'W',
.StatsCommand => 'S',
};
}
@@ -1282,6 +1291,19 @@ pub const Command = struct {
Output.pretty(intro_text, .{});
Output.flush();
},
.StatsCommand => {
const intro_text =
\\<b>Usage<r>: <b><green>bun stats<r>
\\Generate a comprehensive code statistics report for your project
\\
\\Shows file counts, lines of code, imports/exports, and more.
\\Analyzes JavaScript, TypeScript, CSS, and JSON files.
\\
;
Output.pretty(intro_text, .{});
Output.flush();
},
else => {
HelpCommand.printWithReason(.explicit);
},

View File

@@ -1114,6 +1114,14 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
entry_points = entry_points[1..];
}
},
.StatsCommand => {
if (entry_points.len > 0 and strings.eqlComptime(
entry_points[0],
"stats",
)) {
entry_points = entry_points[1..];
}
},
else => {},
}

726
src/cli/stats_command.zig Normal file
View File

@@ -0,0 +1,726 @@
// Custom bundler generation that continues despite errors
fn generateForStats(
transpiler_: *transpiler.Transpiler,
alloc: std.mem.Allocator,
event_loop: bun.jsc.AnyEventLoop,
scanner: *BundleV2.DependenciesScanner,
) !void {
var this = BundleV2.init(
transpiler_,
null,
alloc,
event_loop,
false, // no hot reload
null,
.init(),
) catch {
// Even initialization errors should be ignored
return;
};
this.unique_key = bundle_v2.generateUniqueKey();
// Clear any initialization errors
this.transpiler.log.msgs.clearRetainingCapacity();
this.transpiler.log.errors = 0;
this.transpiler.log.warnings = 0;
// Enqueue entry points but continue even if there are errors
this.enqueueEntryPoints(.normal, this.transpiler.options.entry_points) catch {
// Continue anyway
};
// Clear errors again
this.transpiler.log.msgs.clearRetainingCapacity();
this.transpiler.log.errors = 0;
this.transpiler.log.warnings = 0;
// Wait for parsing to complete
this.waitForParse();
// Clear errors again
this.transpiler.log.msgs.clearRetainingCapacity();
this.transpiler.log.errors = 0;
this.transpiler.log.warnings = 0;
// Scan for secondary paths
this.scanForSecondaryPaths();
// Process server components but ignore errors
this.processServerComponentManifestFiles() catch {};
// Find reachable files
const reachable_files = this.findReachableFiles() catch {
// Return empty if we can't find reachable files
return;
};
// Call the scanner with whatever we managed to collect
this.getAllDependencies(reachable_files, scanner) catch {
// Continue even if dependency scanning fails
};
}
pub const StatsCommand = struct {
const FileStats = struct {
files: u32 = 0,
lines: u32 = 0,
loc: u32 = 0,
imports: u32 = 0,
exports: u32 = 0,
classes: u32 = 0,
functions: u32 = 0,
components: u32 = 0,
avg_size: u32 = 0,
};
const CategoryStats = struct {
typescript: FileStats = .{},
javascript: FileStats = .{},
jsx: FileStats = .{},
commonjs: FileStats = .{},
esmodules: FileStats = .{},
css: FileStats = .{},
json: FileStats = .{},
tests: FileStats = .{},
node_modules: FileStats = .{},
workspace_packages: std.StringHashMap(FileStats),
total: FileStats = .{},
components: u32 = 0,
};
const StatsContext = struct {
stats: CategoryStats,
allocator: std.mem.Allocator,
workspace_packages: [][]const u8,
};
fn countLinesAndLOC(content: []const u8) struct { lines: u32, loc: u32 } {
var lines: u32 = if (content.len > 0) 1 else 0;
var loc: u32 = 0;
var i: usize = 0;
var line_start: usize = 0;
var in_block_comment = false;
while (i < content.len) : (i += 1) {
if (content[i] == '\n') {
const line = content[line_start..i];
const trimmed = std.mem.trim(u8, line, " \t\r");
// Check for block comments
if (std.mem.indexOf(u8, trimmed, "/*") != null) {
in_block_comment = true;
}
if (std.mem.indexOf(u8, trimmed, "*/") != null) {
in_block_comment = false;
} else if (trimmed.len > 0 and
!in_block_comment and
!std.mem.startsWith(u8, trimmed, "//"))
{
loc += 1;
}
lines += 1;
line_start = i + 1;
}
}
// Handle last line without newline
if (line_start < content.len) {
const line = content[line_start..];
const trimmed = std.mem.trim(u8, line, " \t\r");
if (trimmed.len > 0 and !in_block_comment and !std.mem.startsWith(u8, trimmed, "//")) {
loc += 1;
}
}
return .{ .lines = lines, .loc = loc };
}
fn addStats(dest: *FileStats, src: *const FileStats) void {
dest.files += src.files;
dest.lines += src.lines;
dest.loc += src.loc;
dest.imports += src.imports;
dest.exports += src.exports;
dest.classes += src.classes;
dest.functions += src.functions;
dest.components += src.components;
// Recalculate average
if (dest.files > 0) {
dest.avg_size = dest.loc / dest.files;
}
}
fn printTable(stats: *const CategoryStats, workspace_package_names: []const []const u8) void {
_ = workspace_package_names;
// Table with proper column widths for large numbers
Output.pretty("┌{s:─<18}┬{s:─<8}┬{s:─<10}┬{s:─<10}┬{s:─<8}┬{s:─<5}┐\n", .{ "", "", "", "", "", "" });
Output.pretty("│ {s:<16} │ {s:>6} │ {s:>8} │ {s:>8} │ {s:>6} │ {s:>3} │\n", .{ "Category", "Files", "Lines", "LOC", "Funcs", "F/M" });
Output.pretty("├{s:─<18}┼{s:─<8}┼{s:─<10}┼{s:─<10}┼{s:─<8}┼{s:─<5}┤\n", .{ "", "", "", "", "", "" });
const printRow = struct {
fn print(name: []const u8, s: *const FileStats) void {
const functions_per_module: f64 = if (s.files > 0) @as(f64, @floatFromInt(s.functions)) / @as(f64, @floatFromInt(s.files)) else 0;
Output.pretty("│ {s:<16} │ {d:>6} │ {d:>8} │ {d:>8} │ {d:>6} │ {d:>3.0} │\n", .{
name,
s.files,
s.lines,
s.loc,
s.functions,
functions_per_module,
});
}
}.print;
// Language breakdown (all files)
if (stats.javascript.files > 0) {
printRow("JavaScript", &stats.javascript);
}
if (stats.jsx.files > 0) {
printRow("JSX", &stats.jsx);
}
if (stats.typescript.files > 0) {
printRow("TypeScript", &stats.typescript);
}
// Assets
if (stats.json.files > 0) {
printRow("JSON", &stats.json);
}
if (stats.css.files > 0) {
printRow("Stylesheets", &stats.css);
}
// Module Systems (all files)
if (stats.commonjs.files > 0) {
printRow("CommonJS Modules", &stats.commonjs);
}
if (stats.esmodules.files > 0) {
printRow("ECMA Modules", &stats.esmodules);
}
// Separator before summary sections
Output.pretty("├{s:─<18}┼{s:─<8}┼{s:─<10}┼{s:─<10}┼{s:─<8}┼{s:─<5}┤\n", .{ "", "", "", "", "", "" });
// Dependencies
if (stats.node_modules.files > 0) {
printRow("node_modules", &stats.node_modules);
}
// Your code (everything except node_modules)
const your_code = FileStats{
.files = stats.total.files -| stats.node_modules.files,
.lines = stats.total.lines -| stats.node_modules.lines,
.loc = stats.total.loc -| stats.node_modules.loc,
.functions = stats.total.functions -| stats.node_modules.functions,
.imports = stats.total.imports -| stats.node_modules.imports,
.exports = stats.total.exports -| stats.node_modules.exports,
.classes = stats.total.classes -| stats.node_modules.classes,
.components = stats.components,
.avg_size = 0,
};
printRow("Your Code", &your_code);
// Tests
if (stats.tests.files > 0) {
printRow("Tests", &stats.tests);
}
// All code
printRow("All Code", &stats.total);
Output.pretty("└{s:─<18}┴{s:─<8}┴{s:─<10}┴{s:─<10}┴{s:─<8}┴{s:─<5}┘\n", .{ "", "", "", "", "", "" });
// Print interesting metrics
Output.pretty("\n📊 Insights:\n", .{});
const code_loc = your_code.loc -| stats.tests.loc;
const test_loc = stats.tests.loc;
// Test coverage
if (code_loc > 0 and test_loc > 0) {
const coverage = (@as(f64, @floatFromInt(test_loc)) / @as(f64, @floatFromInt(code_loc + test_loc))) * 100.0;
Output.pretty(" • Test coverage: {d:.1}%\n", .{coverage});
}
// TypeScript adoption (excluding node_modules)
const ts_files = stats.typescript.files;
const js_files = stats.javascript.files + stats.jsx.files;
if (ts_files > 0 and js_files > 0) {
const ts_adoption = (@as(f64, @floatFromInt(ts_files)) / @as(f64, @floatFromInt(ts_files + js_files))) * 100.0;
Output.pretty(" • TypeScript: {d:.1}%\n", .{ts_adoption});
}
// ES Modules adoption (excluding node_modules)
if (stats.esmodules.files > 0 and stats.commonjs.files > 0) {
const esm_adoption = (@as(f64, @floatFromInt(stats.esmodules.files)) / @as(f64, @floatFromInt(stats.esmodules.files + stats.commonjs.files))) * 100.0;
Output.pretty(" • ES Modules: {d:.1}%\n", .{esm_adoption});
}
// Average file size
if (your_code.files > 0) {
const avg_size = @as(f64, @floatFromInt(your_code.loc)) / @as(f64, @floatFromInt(your_code.files));
Output.pretty(" • Avg file size: {d:.0} LOC\n", .{avg_size});
}
// Average function size
if (your_code.functions > 0) {
const avg_func_size = @as(f64, @floatFromInt(your_code.loc)) / @as(f64, @floatFromInt(your_code.functions));
Output.pretty(" • Avg function: {d:.0} LOC\n", .{avg_func_size});
}
// Dependency weight
if (stats.node_modules.files > 0 and your_code.files > 0) {
const dep_ratio = @as(f64, @floatFromInt(stats.node_modules.loc)) / @as(f64, @floatFromInt(your_code.loc));
Output.pretty(" • Dependency weight: {d:.1}x your code\n", .{dep_ratio});
}
}
fn printSummary(stats: *const CategoryStats, workspace_count: usize, reachable_count: usize, source_size: u64, elapsed_ms: u64) void {
_ = workspace_count;
_ = reachable_count;
_ = source_size;
_ = elapsed_ms;
_ = stats;
// All summary output has been removed - table is sufficient
}
fn getWorkspacePackages(allocator: std.mem.Allocator) ![][]const u8 {
// For now, return empty list
// TODO: Parse package.json properly
return allocator.alloc([]const u8, 0) catch &.{};
}
fn onStatsCollect(
ctx_: *anyopaque,
result: *BundleV2.DependenciesScanner.Result,
) anyerror!void {
const ctx = @as(*StatsContext, @ptrCast(@alignCast(ctx_)));
const bundle = result.bundle_v2;
// Access the parsed graph data
const graph = &bundle.graph;
const ast_data = &graph.ast;
// Get the MultiArrayList slices
const sources = graph.input_files.items(.source);
const loaders = graph.input_files.items(.loader);
const import_records = ast_data.items(.import_records);
const exports_kind = ast_data.items(.exports_kind);
const named_exports = ast_data.items(.named_exports);
const export_star_import_records = ast_data.items(.export_star_import_records);
const parts_list = ast_data.items(.parts);
// Process each reachable file
for (result.reachable_files) |source_index| {
const index = source_index.get();
// Comprehensive bounds checking for all arrays
if (index >= sources.len or index >= loaders.len) continue;
// Skip the runtime file (index 0)
if (index == 0) continue;
const source = sources[index];
const loader = loaders[index];
const imports = if (index >= import_records.len) ImportRecord.List{} else import_records[index];
const export_kind = if (index >= exports_kind.len) .none else exports_kind[index];
// Only access named_exports and export_stars for non-CSS files
const is_css = loader == .css;
const named_exports_count: u32 = if (is_css or index >= named_exports.len) 0 else @intCast(named_exports[index].count());
const export_stars_count: u32 = if (is_css or index >= export_star_import_records.len) 0 else @intCast(export_star_import_records[index].len);
// Get source content and path
const source_contents = source.contents;
const path_text = source.path.text;
// Skip virtual files and bun: files
if (strings.hasPrefixComptime(path_text, "bun:") or
strings.hasPrefixComptime(path_text, "node:") or
strings.hasPrefixComptime(path_text, "<") or
strings.eqlComptime(path_text, "bun")) continue;
// Count lines and LOC
const line_stats = countLinesAndLOC(source_contents);
// Categorize file
const is_test = std.mem.indexOf(u8, path_text, ".test.") != null or
std.mem.indexOf(u8, path_text, ".spec.") != null or
std.mem.indexOf(u8, path_text, "__tests__") != null;
const is_node_modules = std.mem.indexOf(u8, path_text, "node_modules") != null;
// Determine workspace package
var workspace_pkg: ?[]const u8 = null;
for (ctx.workspace_packages) |pkg_name| {
if (std.mem.indexOf(u8, path_text, pkg_name) != null and !is_node_modules) {
workspace_pkg = pkg_name;
break;
}
}
// Count imports and exports
const import_count: u32 = @intCast(imports.len);
const export_count: u32 = named_exports_count + export_stars_count;
// Count classes and functions using the parsed AST (for non-CSS files)
var class_count: u32 = 0;
var function_count: u32 = 0;
// Only access parts for non-CSS files
// When parts.len == 0, it means the AST is invalid/failed to parse
// Skip files that failed to parse or have empty ASTs
if (!is_css and index < parts_list.len and parts_list[index].len > 0) {
// Try to safely access the parts
const parts = parts_list[index].slice();
// Iterate through all parts in the file
for (parts) |part| {
// Iterate through all statements in the part
for (part.stmts) |stmt| {
switch (stmt.data) {
// Direct function declarations
.s_function => {
function_count += 1;
},
// Direct class declarations
.s_class => {
class_count += 1;
},
// Local variable declarations (const/let/var)
.s_local => |local| {
// Check each declaration's value
for (local.decls.slice()) |decl| {
if (decl.value) |value_expr| {
switch (value_expr.data) {
.e_function => function_count += 1,
.e_arrow => function_count += 1,
.e_class => class_count += 1,
else => {},
}
}
}
},
// Expression statements (e.g., anonymous functions)
.s_expr => |expr_stmt| {
switch (expr_stmt.value.data) {
.e_function => function_count += 1,
.e_arrow => function_count += 1,
.e_class => class_count += 1,
// Check for assignments that might contain functions/classes
.e_binary => |binary| {
if (binary.op == .bin_assign) {
switch (binary.right.data) {
.e_function => function_count += 1,
.e_arrow => function_count += 1,
.e_class => class_count += 1,
else => {},
}
}
},
else => {},
}
},
// Export statements might also contain functions/classes
.s_export_default => |export_default| {
switch (export_default.value) {
.stmt => |export_stmt| {
switch (export_stmt.data) {
.s_function => function_count += 1,
.s_class => class_count += 1,
else => {},
}
},
.expr => |export_expr| {
switch (export_expr.data) {
.e_function => function_count += 1,
.e_arrow => function_count += 1,
.e_class => class_count += 1,
else => {},
}
},
}
},
else => {},
}
}
}
}
var file_stats = FileStats{
.files = 1,
.lines = line_stats.lines,
.loc = line_stats.loc,
.imports = import_count,
.exports = export_count,
.classes = class_count,
.functions = function_count,
.components = 0,
.avg_size = line_stats.loc,
};
// Determine module type from exports_kind
const is_commonjs = export_kind == .cjs;
const is_esm = export_kind == .esm;
// Check categories first - node_modules files should ONLY count in node_modules
if (is_node_modules) {
// Files in node_modules only count towards node_modules stats
file_stats.imports = 0;
file_stats.exports = 0;
addStats(&ctx.stats.node_modules, &file_stats);
} else {
// Files NOT in node_modules get categorized normally
switch (loader) {
.tsx, .ts => {
addStats(&ctx.stats.typescript, &file_stats);
if (is_commonjs) {
addStats(&ctx.stats.commonjs, &file_stats);
} else if (is_esm) {
addStats(&ctx.stats.esmodules, &file_stats);
}
},
.jsx => {
addStats(&ctx.stats.jsx, &file_stats);
if (is_commonjs) {
addStats(&ctx.stats.commonjs, &file_stats);
} else if (is_esm) {
addStats(&ctx.stats.esmodules, &file_stats);
}
},
.js => {
addStats(&ctx.stats.javascript, &file_stats);
if (is_commonjs) {
addStats(&ctx.stats.commonjs, &file_stats);
} else if (is_esm) {
addStats(&ctx.stats.esmodules, &file_stats);
}
},
.css => {
file_stats.imports = 0;
file_stats.exports = 0;
addStats(&ctx.stats.css, &file_stats);
},
.json => {
file_stats.imports = 0;
file_stats.exports = 0;
addStats(&ctx.stats.json, &file_stats);
},
else => {},
}
// Check if it's a test file
if (is_test) {
addStats(&ctx.stats.tests, &file_stats);
} else if (workspace_pkg) |pkg| {
if (ctx.stats.workspace_packages.getPtr(pkg)) |pkg_stats| {
addStats(pkg_stats, &file_stats);
}
}
}
// No need to track components
// Always add to total
addStats(&ctx.stats.total, &file_stats);
}
}
fn findAllJSFiles(allocator: std.mem.Allocator, dir_path: []const u8) ![][]const u8 {
var files = std.ArrayList([]const u8).init(allocator);
errdefer {
for (files.items) |file| {
allocator.free(file);
}
files.deinit();
}
// Simple recursive directory walker
var stack = std.ArrayList([]const u8).init(allocator);
defer {
for (stack.items) |item| {
allocator.free(item);
}
stack.deinit();
}
try stack.append(try allocator.dupe(u8, dir_path));
while (stack.items.len > 0) {
const current_dir = stack.pop() orelse break;
defer allocator.free(current_dir);
// Skip node_modules and hidden directories
if (std.mem.indexOf(u8, current_dir, "node_modules") != null or
std.mem.indexOf(u8, current_dir, "/.git") != null or
std.mem.indexOf(u8, current_dir, "/.next") != null) continue;
var dir = std.fs.openDirAbsolute(current_dir, .{ .iterate = true }) catch |err| {
if (err == error.NotDir or err == error.FileNotFound) continue;
return err;
};
defer dir.close();
var iter = dir.iterate();
while (try iter.next()) |entry| {
const full_path = try std.fs.path.join(allocator, &.{ current_dir, entry.name });
switch (entry.kind) {
.directory => {
// Add directory to stack for processing
try stack.append(full_path);
},
.file => {
// Check if it's a JS/TS/JSON/CSS file
const ext = std.fs.path.extension(entry.name);
const is_js_file = std.mem.eql(u8, ext, ".js") or
std.mem.eql(u8, ext, ".jsx") or
std.mem.eql(u8, ext, ".ts") or
std.mem.eql(u8, ext, ".tsx") or
std.mem.eql(u8, ext, ".mjs") or
std.mem.eql(u8, ext, ".cjs") or
std.mem.eql(u8, ext, ".mts") or
std.mem.eql(u8, ext, ".cts") or
std.mem.eql(u8, ext, ".json") or
std.mem.eql(u8, ext, ".css");
if (is_js_file) {
try files.append(full_path);
} else {
allocator.free(full_path);
}
},
else => {
allocator.free(full_path);
},
}
}
}
return files.toOwnedSlice();
}
pub fn exec(ctx: Command.Context) !void {
Global.configureAllocator(.{ .long_running = true });
const allocator = ctx.allocator;
const start_time = std.time.nanoTimestamp();
// Set up the bundler context to be as permissive as possible
ctx.args.target = .bun; // Use bun target to resolve test files and Bun-specific imports
ctx.args.packages = .bundle; // Bundle mode to analyze all files
ctx.args.ignore_dce_annotations = true; // Ignore DCE annotations that might cause errors
// Get workspace packages
const workspace_packages = try getWorkspacePackages(allocator);
defer {
for (workspace_packages) |pkg| {
allocator.free(pkg);
}
allocator.free(workspace_packages);
}
// Initialize stats context
var stats_ctx = StatsContext{
.stats = CategoryStats{
.workspace_packages = std.StringHashMap(FileStats).init(allocator),
},
.allocator = allocator,
.workspace_packages = workspace_packages,
};
defer stats_ctx.stats.workspace_packages.deinit();
// Initialize workspace package stats
for (workspace_packages) |pkg| {
try stats_ctx.stats.workspace_packages.put(pkg, FileStats{});
}
// Set up transpiler with suppressed error logging
var silent_log = Logger.Log.init(allocator);
silent_log.level = .err; // Highest level to minimize processing
var this_transpiler = try transpiler.Transpiler.init(allocator, &silent_log, ctx.args, null);
// Handle entry points based on user input
var allocated_entry_points: ?[][]const u8 = null;
defer if (allocated_entry_points) |entry_points| {
for (entry_points) |entry| {
allocator.free(entry);
}
allocator.free(entry_points);
};
if (ctx.args.entry_points.len > 0) {
// User provided entry points - use them directly
this_transpiler.options.entry_points = ctx.args.entry_points;
} else {
// No entry points provided - walk directory to find all JS/TS files
const cwd = try std.process.getCwdAlloc(allocator);
defer allocator.free(cwd);
allocated_entry_points = try findAllJSFiles(allocator, cwd);
this_transpiler.options.entry_points = allocated_entry_points.?;
}
this_transpiler.options.output_dir = ""; // No output needed
this_transpiler.options.write = false; // Don't write files
this_transpiler.configureLinker();
try this_transpiler.configureDefines();
// Set up the dependencies scanner to collect stats
var scanner = BundleV2.DependenciesScanner{
.ctx = &stats_ctx,
.entry_points = this_transpiler.options.entry_points,
.onFetch = onStatsCollect,
};
// Run the bundler to parse all files
if (this_transpiler.options.entry_points.len == 0) {
Output.prettyErrorln("<red>error<r>: No files found to analyze", .{});
return;
}
// No "Analyzing X files..." message - just start processing
// Clear any messages and reset error count
this_transpiler.log.msgs.clearRetainingCapacity();
this_transpiler.log.errors = 0;
this_transpiler.log.warnings = 0;
// Use our custom generation function that doesn't stop on errors
try generateForStats(
&this_transpiler,
allocator,
bun.jsc.AnyEventLoop.init(allocator),
&scanner,
);
// Calculate elapsed time
const end_time = std.time.nanoTimestamp();
const elapsed_ns = @as(u64, @intCast(end_time - start_time));
const elapsed_ms = elapsed_ns / std.time.ns_per_ms;
// Print results
printTable(&stats_ctx.stats, workspace_packages);
printSummary(&stats_ctx.stats, workspace_packages.len, 0, 0, elapsed_ms);
}
};
const Logger = @import("../logger.zig");
const options = @import("../options.zig");
const std = @import("std");
const transpiler = @import("../transpiler.zig");
const Command = @import("../cli.zig").Command;
const ImportRecord = @import("../import_record.zig").ImportRecord;
const bundle_v2 = @import("../bundler/bundle_v2.zig");
const BundleV2 = bundle_v2.BundleV2;
const bun = @import("bun");
const Global = bun.Global;
const Output = bun.Output;
const ast = bun.ast;
const strings = bun.strings;

View File

@@ -0,0 +1,127 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("bun stats - basic functionality", async () => {
using dir = tempDir("stats-test", {
"index.js": `console.log("hello");`,
"utils.mjs": `export const add = (a, b) => a + b;`,
"config.json": `{"name": "test"}`,
"styles.css": `body { color: red; }`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "stats"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
// Check that the output contains expected sections
expect(stdout).toContain("JavaScript");
expect(stdout).toContain("ES modules");
expect(stdout).toContain("CSS");
expect(stdout).toContain("JSON");
expect(stdout).toContain("Total");
expect(stdout).toContain("Code LOC:");
expect(stdout).toContain("Bundled Size (est.):");
});
test("bun stats - with TypeScript files", async () => {
using dir = tempDir("stats-ts-test", {
"index.ts": `const msg: string = "hello";\nconsole.log(msg);`,
"types.d.ts": `export interface User { name: string; }`,
"test.spec.ts": `import { test } from "bun:test";\ntest("sample", () => {});`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "stats"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
// Check TypeScript stats
expect(stdout).toContain("TypeScript");
expect(stdout).toContain("Tests");
expect(stdout).toContain("Test LOC:");
});
test("bun stats - handles CommonJS and ES modules", async () => {
using dir = tempDir("stats-modules-test", {
"cjs-module.js": `module.exports = { foo: 'bar' };`,
"esm-module.mjs": `export default { foo: 'bar' };`,
"mixed.js": `const lib = require('./lib');\nexport { lib };`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "stats"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
// Check module type detection
expect(stdout).toContain("CommonJS modules");
expect(stdout).toContain("ES modules");
});
test("bun stats - counts imports and exports", async () => {
using dir = tempDir("stats-imports-test", {
"module.js": `
import React from 'react';
import { useState } from 'react';
import './styles.css';
export default App;
export { helper };
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "stats"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout] = await Promise.all([proc.stdout.text(), proc.exited]);
// Should count imports and exports - check the table contains expected values
expect(stdout).toContain("| 3 | 2 |");
});
test("bun stats --help", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "stats", "--help"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(stdout).toContain("Usage:");
expect(stdout).toContain("bun stats");
expect(stdout).toContain("Generate a comprehensive code statistics report");
});