@densetsuuu/docteur 0.1.1-beta
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 +9 -0
- package/README.md +155 -0
- package/build/index.d.ts +23 -0
- package/build/index.js +23 -0
- package/build/src/cli/commands/diagnose.d.ts +27 -0
- package/build/src/cli/commands/diagnose.js +67 -0
- package/build/src/cli/commands/xray.d.ts +7 -0
- package/build/src/cli/commands/xray.js +49 -0
- package/build/src/cli/main.d.ts +2 -0
- package/build/src/cli/main.js +30 -0
- package/build/src/profiler/collector.d.ts +16 -0
- package/build/src/profiler/collector.js +142 -0
- package/build/src/profiler/hooks.d.ts +27 -0
- package/build/src/profiler/hooks.js +45 -0
- package/build/src/profiler/loader.d.ts +1 -0
- package/build/src/profiler/loader.js +111 -0
- package/build/src/profiler/profiler.d.ts +9 -0
- package/build/src/profiler/profiler.js +117 -0
- package/build/src/profiler/registries/categories.d.ts +7 -0
- package/build/src/profiler/registries/categories.js +87 -0
- package/build/src/profiler/registries/index.d.ts +2 -0
- package/build/src/profiler/registries/index.js +2 -0
- package/build/src/profiler/registries/symbols.d.ts +42 -0
- package/build/src/profiler/registries/symbols.js +71 -0
- package/build/src/profiler/reporters/base_reporter.d.ts +19 -0
- package/build/src/profiler/reporters/base_reporter.js +9 -0
- package/build/src/profiler/reporters/console_reporter.d.ts +8 -0
- package/build/src/profiler/reporters/console_reporter.js +237 -0
- package/build/src/profiler/reporters/format.d.ts +62 -0
- package/build/src/profiler/reporters/format.js +84 -0
- package/build/src/profiler/reporters/tui_reporter.d.ts +8 -0
- package/build/src/profiler/reporters/tui_reporter.js +32 -0
- package/build/src/types.d.ts +167 -0
- package/build/src/types.js +9 -0
- package/build/src/xray/components/ListView.d.ts +9 -0
- package/build/src/xray/components/ListView.js +69 -0
- package/build/src/xray/components/ModuleView.d.ts +9 -0
- package/build/src/xray/components/ModuleView.js +144 -0
- package/build/src/xray/components/ProviderListView.d.ts +8 -0
- package/build/src/xray/components/ProviderListView.js +57 -0
- package/build/src/xray/components/ProviderView.d.ts +7 -0
- package/build/src/xray/components/ProviderView.js +35 -0
- package/build/src/xray/components/XRayApp.d.ts +7 -0
- package/build/src/xray/components/XRayApp.js +78 -0
- package/build/src/xray/tree.d.ts +22 -0
- package/build/src/xray/tree.js +85 -0
- package/package.json +110 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Console Reporter
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| Renders profiling results to the terminal using @poppinss/cliui.
|
|
7
|
+
|
|
|
8
|
+
*/
|
|
9
|
+
import { ProfileCollector } from '../collector.js';
|
|
10
|
+
import { symbols } from '../registries/index.js';
|
|
11
|
+
import { colorDuration, createBar, formatDuration, getCategoryIcon, getEffectiveTime, simplifyUrl, ui, } from './format.js';
|
|
12
|
+
export class ConsoleReporter {
|
|
13
|
+
/**
|
|
14
|
+
* Renders the complete report to console.
|
|
15
|
+
*/
|
|
16
|
+
render(context) {
|
|
17
|
+
const { result, config, cwd } = context;
|
|
18
|
+
this.#printHeader();
|
|
19
|
+
this.#printSummary(result);
|
|
20
|
+
this.#printAppFiles(result, cwd);
|
|
21
|
+
this.#printSlowestModules(result, config, cwd);
|
|
22
|
+
if (config.groupByPackage) {
|
|
23
|
+
this.#printPackageGroups(result, config);
|
|
24
|
+
}
|
|
25
|
+
this.#printProviders(result);
|
|
26
|
+
this.#printRecommendations(result, config);
|
|
27
|
+
this.#printFooter();
|
|
28
|
+
}
|
|
29
|
+
#printHeader() {
|
|
30
|
+
ui.logger.log('');
|
|
31
|
+
ui.logger.log(ui.colors.bold(ui.colors.cyan(` ${symbols.stethoscope} Docteur - Cold Start Analysis`)));
|
|
32
|
+
ui.logger.log(ui.colors.dim(` ${symbols.dash}`.repeat(25)));
|
|
33
|
+
ui.logger.log('');
|
|
34
|
+
}
|
|
35
|
+
#printSummary(result) {
|
|
36
|
+
ui.logger.log(ui.colors.bold(` ${symbols.chart} Summary`));
|
|
37
|
+
ui.logger.log('');
|
|
38
|
+
const table = ui.table();
|
|
39
|
+
table
|
|
40
|
+
.row([ui.colors.dim('Total boot time:'), colorDuration(result.totalTime)])
|
|
41
|
+
.row([
|
|
42
|
+
ui.colors.dim('Total modules loaded:'),
|
|
43
|
+
ui.colors.white(result.summary.totalModules.toString()),
|
|
44
|
+
])
|
|
45
|
+
.row([
|
|
46
|
+
ui.colors.dim(' App modules:'),
|
|
47
|
+
ui.colors.white(result.summary.userModules.toString()),
|
|
48
|
+
])
|
|
49
|
+
.row([
|
|
50
|
+
ui.colors.dim(' Node modules:'),
|
|
51
|
+
ui.colors.white(result.summary.nodeModules.toString()),
|
|
52
|
+
])
|
|
53
|
+
.row([
|
|
54
|
+
ui.colors.dim(' AdonisJS modules:'),
|
|
55
|
+
ui.colors.white(result.summary.adonisModules.toString()),
|
|
56
|
+
])
|
|
57
|
+
.row([ui.colors.dim('Module import time:'), colorDuration(result.summary.totalModuleTime)]);
|
|
58
|
+
if (result.providers.length > 0) {
|
|
59
|
+
table.row([
|
|
60
|
+
ui.colors.dim('Provider exec time:'),
|
|
61
|
+
colorDuration(result.summary.totalProviderTime),
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
table.render();
|
|
65
|
+
ui.logger.log('');
|
|
66
|
+
}
|
|
67
|
+
#printSlowestModules(result, config, cwd) {
|
|
68
|
+
const collector = new ProfileCollector(result.modules, result.providers);
|
|
69
|
+
const filtered = collector.filterModules(config);
|
|
70
|
+
const slowest = new ProfileCollector(filtered).getTopSlowest(config.topModules);
|
|
71
|
+
if (slowest.length === 0) {
|
|
72
|
+
ui.logger.log(ui.colors.dim(' No modules found above the threshold'));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const maxTime = slowest[0] ? getEffectiveTime(slowest[0]) : 1;
|
|
76
|
+
ui.logger.log(ui.colors.bold(` ${symbols.turtle} Slowest Module Imports (top ${config.topModules})`));
|
|
77
|
+
ui.logger.log(ui.colors.dim(' Total = with dependencies, Self = file only'));
|
|
78
|
+
ui.logger.log('');
|
|
79
|
+
const table = ui.table();
|
|
80
|
+
table
|
|
81
|
+
.head([
|
|
82
|
+
ui.colors.dim('#'),
|
|
83
|
+
ui.colors.dim('Module'),
|
|
84
|
+
ui.colors.dim('Total'),
|
|
85
|
+
ui.colors.dim('Self'),
|
|
86
|
+
ui.colors.dim(''),
|
|
87
|
+
])
|
|
88
|
+
.columnWidths([6, 45, 11, 11, 30]);
|
|
89
|
+
slowest.forEach((module, index) => {
|
|
90
|
+
const simplified = simplifyUrl(module.resolvedUrl, cwd);
|
|
91
|
+
const totalTime = getEffectiveTime(module);
|
|
92
|
+
const selfTime = module.loadTime;
|
|
93
|
+
table.row([
|
|
94
|
+
ui.colors.dim((index + 1).toString()),
|
|
95
|
+
simplified.length > 40 ? simplified.slice(-40) : simplified,
|
|
96
|
+
colorDuration(totalTime),
|
|
97
|
+
ui.colors.dim(formatDuration(selfTime)),
|
|
98
|
+
createBar(totalTime, maxTime, 25),
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
table.render();
|
|
102
|
+
ui.logger.log('');
|
|
103
|
+
}
|
|
104
|
+
#printPackageGroups(result, config) {
|
|
105
|
+
const collector = new ProfileCollector(result.modules, result.providers);
|
|
106
|
+
const filtered = collector.filterModules(config);
|
|
107
|
+
const groups = new ProfileCollector(filtered).groupModulesByPackage();
|
|
108
|
+
if (groups.length === 0) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const topGroups = groups.slice(0, 10);
|
|
112
|
+
const maxTime = topGroups[0]?.totalTime || 1;
|
|
113
|
+
ui.logger.log(ui.colors.bold(` ${symbols.package} Slowest Packages`));
|
|
114
|
+
ui.logger.log(ui.colors.dim(' Total import time per npm package'));
|
|
115
|
+
ui.logger.log('');
|
|
116
|
+
const table = ui.table();
|
|
117
|
+
table
|
|
118
|
+
.head([
|
|
119
|
+
ui.colors.dim('#'),
|
|
120
|
+
ui.colors.dim('Package'),
|
|
121
|
+
ui.colors.dim('Modules'),
|
|
122
|
+
ui.colors.dim('Total'),
|
|
123
|
+
ui.colors.dim(''),
|
|
124
|
+
])
|
|
125
|
+
.columnWidths([6, 38, 12, 12, 35]);
|
|
126
|
+
topGroups.forEach((group, index) => {
|
|
127
|
+
table.row([
|
|
128
|
+
ui.colors.dim((index + 1).toString()),
|
|
129
|
+
group.name.length > 33 ? group.name.slice(0, 33) : group.name,
|
|
130
|
+
ui.colors.dim(group.modules.length.toString()),
|
|
131
|
+
colorDuration(group.totalTime),
|
|
132
|
+
createBar(group.totalTime, maxTime),
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
table.render();
|
|
136
|
+
ui.logger.log('');
|
|
137
|
+
}
|
|
138
|
+
#printProviders(result) {
|
|
139
|
+
if (result.providers.length === 0) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const sorted = [...result.providers].sort((a, b) => b.totalTime - a.totalTime);
|
|
143
|
+
const maxTime = sorted[0]?.totalTime || 1;
|
|
144
|
+
ui.logger.log(ui.colors.bold(` ${symbols.lightning} Provider Execution Times`));
|
|
145
|
+
ui.logger.log(ui.colors.dim(' Time spent in register() and boot() methods'));
|
|
146
|
+
ui.logger.log('');
|
|
147
|
+
const table = ui.table();
|
|
148
|
+
table
|
|
149
|
+
.head([
|
|
150
|
+
ui.colors.dim('#'),
|
|
151
|
+
ui.colors.dim('Provider'),
|
|
152
|
+
ui.colors.dim('Register'),
|
|
153
|
+
ui.colors.dim('Boot'),
|
|
154
|
+
ui.colors.dim('Total'),
|
|
155
|
+
ui.colors.dim(''),
|
|
156
|
+
])
|
|
157
|
+
.columnWidths([5, 28, 11, 11, 11, 35]);
|
|
158
|
+
sorted.forEach((provider, index) => {
|
|
159
|
+
table.row([
|
|
160
|
+
ui.colors.dim((index + 1).toString()),
|
|
161
|
+
provider.name.length > 26 ? provider.name.slice(0, 26) : provider.name,
|
|
162
|
+
colorDuration(provider.registerTime),
|
|
163
|
+
colorDuration(provider.bootTime),
|
|
164
|
+
colorDuration(provider.totalTime),
|
|
165
|
+
createBar(provider.totalTime, maxTime, 30),
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
table.render();
|
|
169
|
+
ui.logger.log('');
|
|
170
|
+
}
|
|
171
|
+
#printRecommendations(result, config) {
|
|
172
|
+
const recommendations = [];
|
|
173
|
+
if (result.totalTime > 2000) {
|
|
174
|
+
recommendations.push('Total boot time is over 2s. Consider lazy-loading some providers.');
|
|
175
|
+
}
|
|
176
|
+
if (result.summary.totalModules > 500) {
|
|
177
|
+
recommendations.push(`Loading ${result.summary.totalModules} modules. Consider code splitting or lazy imports.`);
|
|
178
|
+
}
|
|
179
|
+
const collector = new ProfileCollector(result.modules, result.providers);
|
|
180
|
+
const filtered = collector.filterModules(config);
|
|
181
|
+
const verySlowModules = filtered.filter((m) => getEffectiveTime(m) > 100);
|
|
182
|
+
if (verySlowModules.length > 0) {
|
|
183
|
+
recommendations.push(`${verySlowModules.length} module(s) took over 100ms to load. Check for heavy initialization code.`);
|
|
184
|
+
}
|
|
185
|
+
if (recommendations.length === 0) {
|
|
186
|
+
ui.logger.log(ui.colors.bold(` ${symbols.checkmark} `) + ui.colors.green('No major issues detected!'));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
ui.logger.log(ui.colors.bold(` ${symbols.lightbulb} Recommendations`));
|
|
190
|
+
ui.logger.log('');
|
|
191
|
+
recommendations.forEach((rec) => {
|
|
192
|
+
ui.logger.log(` ${ui.colors.yellow(symbols.bullet)} ${rec}`);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
ui.logger.log('');
|
|
196
|
+
}
|
|
197
|
+
#printAppFiles(result, cwd) {
|
|
198
|
+
const groups = result.summary.appFileGroups;
|
|
199
|
+
if (groups.length === 0) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
ui.logger.log(ui.colors.bold(` ${symbols.folder} App Files by Category`));
|
|
203
|
+
ui.logger.log('');
|
|
204
|
+
for (const group of groups) {
|
|
205
|
+
if (group.files.length === 0)
|
|
206
|
+
continue;
|
|
207
|
+
const icon = getCategoryIcon(group.category);
|
|
208
|
+
const header = ` ${icon} ${group.displayName} (${group.files.length} files, ${formatDuration(group.totalTime)})`;
|
|
209
|
+
ui.logger.log(ui.colors.bold(ui.colors.white(header)));
|
|
210
|
+
this.#printAppFileGroup(group, cwd);
|
|
211
|
+
ui.logger.log('');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
#printAppFileGroup(group, cwd) {
|
|
215
|
+
const maxTime = group.files[0] ? getEffectiveTime(group.files[0]) : 1;
|
|
216
|
+
const table = ui.table();
|
|
217
|
+
table.columnWidths([35, 11, 11, 30]);
|
|
218
|
+
for (const file of group.files) {
|
|
219
|
+
const simplified = simplifyUrl(file.resolvedUrl, cwd);
|
|
220
|
+
const fileName = simplified.split('/').pop() || simplified;
|
|
221
|
+
const totalTime = getEffectiveTime(file);
|
|
222
|
+
const selfTime = file.loadTime;
|
|
223
|
+
table.row([
|
|
224
|
+
fileName.length > 33 ? fileName.slice(-33) : fileName,
|
|
225
|
+
colorDuration(totalTime),
|
|
226
|
+
ui.colors.dim(formatDuration(selfTime)),
|
|
227
|
+
createBar(totalTime, maxTime, 25),
|
|
228
|
+
]);
|
|
229
|
+
}
|
|
230
|
+
table.render();
|
|
231
|
+
}
|
|
232
|
+
#printFooter() {
|
|
233
|
+
ui.logger.log(ui.colors.dim(` ${symbols.dash}`.repeat(25)));
|
|
234
|
+
ui.logger.log(ui.colors.dim(' Run with --help for more options'));
|
|
235
|
+
ui.logger.log('');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { AppFileCategory, ModuleTiming } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shared UI instance for consistent styling
|
|
4
|
+
*/
|
|
5
|
+
export declare const ui: {
|
|
6
|
+
colors: import("@poppinss/colors/types").Colors;
|
|
7
|
+
logger: import("@poppinss/cliui").Logger;
|
|
8
|
+
table: (tableOptions
|
|
9
|
+
/**
|
|
10
|
+
* Colors a duration based on how slow it is
|
|
11
|
+
*/
|
|
12
|
+
? /**
|
|
13
|
+
* Colors a duration based on how slow it is
|
|
14
|
+
*/: Partial<import("@poppinss/cliui/types").TableOptions>) => import("@poppinss/cliui").Table;
|
|
15
|
+
tasks: (tasksOptions?: Partial<import("@poppinss/cliui/types").TaskManagerOptions>) => import("@poppinss/cliui").TaskManager;
|
|
16
|
+
steps: () => import("@poppinss/cliui").Steps;
|
|
17
|
+
icons: {
|
|
18
|
+
tick: string;
|
|
19
|
+
cross: string;
|
|
20
|
+
bullet: string;
|
|
21
|
+
nodejs: string;
|
|
22
|
+
pointer: string;
|
|
23
|
+
info: string;
|
|
24
|
+
warning: string;
|
|
25
|
+
squareSmallFilled: string;
|
|
26
|
+
borderVertical: string;
|
|
27
|
+
};
|
|
28
|
+
sticker: () => import("@poppinss/cliui").Instructions;
|
|
29
|
+
instructions: () => import("@poppinss/cliui").Instructions;
|
|
30
|
+
switchMode(modeToUse: "raw" | "silent" | "normal"): void;
|
|
31
|
+
useRenderer(rendererToUse: import("@poppinss/cliui/types").RendererContract): void;
|
|
32
|
+
useColors(colorsToUse: import("@poppinss/colors/types").Colors): void;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Get icon for a category
|
|
36
|
+
*/
|
|
37
|
+
export declare function getCategoryIcon(category: AppFileCategory): string;
|
|
38
|
+
/**
|
|
39
|
+
* Gets the effective time for a module.
|
|
40
|
+
* Uses subtreeTime (total including dependencies) if available,
|
|
41
|
+
* otherwise falls back to loadTime.
|
|
42
|
+
*/
|
|
43
|
+
export declare function getEffectiveTime(module: ModuleTiming): number;
|
|
44
|
+
/**
|
|
45
|
+
* Formats a duration in milliseconds for display
|
|
46
|
+
*/
|
|
47
|
+
export declare function formatDuration(ms: number): string;
|
|
48
|
+
/**
|
|
49
|
+
* Colors a duration based on how slow it is
|
|
50
|
+
*/
|
|
51
|
+
export declare function colorDuration(ms: number): string;
|
|
52
|
+
/**
|
|
53
|
+
* Creates a visual bar representing the duration
|
|
54
|
+
*/
|
|
55
|
+
export declare function createBar(ms: number, maxMs: number, width?: number): string;
|
|
56
|
+
/**
|
|
57
|
+
* Simplifies a module URL for display:
|
|
58
|
+
* - Strips file:// protocol
|
|
59
|
+
* - Converts absolute paths to relative (using cwd)
|
|
60
|
+
* - For node_modules, shows only the package path (handles pnpm store)
|
|
61
|
+
*/
|
|
62
|
+
export declare function simplifyUrl(url: string, cwd: string): string;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| Formatting Utilities
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| Shared formatting functions for report rendering.
|
|
7
|
+
|
|
|
8
|
+
*/
|
|
9
|
+
import { cliui } from '@poppinss/cliui';
|
|
10
|
+
import { categories, symbols } from '../registries/index.js';
|
|
11
|
+
/**
|
|
12
|
+
* Shared UI instance for consistent styling
|
|
13
|
+
*/
|
|
14
|
+
export const ui = cliui();
|
|
15
|
+
/**
|
|
16
|
+
* Get icon for a category
|
|
17
|
+
*/
|
|
18
|
+
export function getCategoryIcon(category) {
|
|
19
|
+
return categories[category].icon;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Gets the effective time for a module.
|
|
23
|
+
* Uses subtreeTime (total including dependencies) if available,
|
|
24
|
+
* otherwise falls back to loadTime.
|
|
25
|
+
*/
|
|
26
|
+
export function getEffectiveTime(module) {
|
|
27
|
+
return module.subtreeTime ?? module.loadTime;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Formats a duration in milliseconds for display
|
|
31
|
+
*/
|
|
32
|
+
export function formatDuration(ms) {
|
|
33
|
+
if (ms >= 1000) {
|
|
34
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
35
|
+
}
|
|
36
|
+
return `${ms.toFixed(2)}ms`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Colors a duration based on how slow it is
|
|
40
|
+
*/
|
|
41
|
+
export function colorDuration(ms) {
|
|
42
|
+
const formatted = formatDuration(ms);
|
|
43
|
+
if (ms >= 100)
|
|
44
|
+
return ui.colors.red(formatted);
|
|
45
|
+
if (ms >= 50)
|
|
46
|
+
return ui.colors.yellow(formatted);
|
|
47
|
+
if (ms >= 10)
|
|
48
|
+
return ui.colors.cyan(formatted);
|
|
49
|
+
return ui.colors.green(formatted);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Creates a visual bar representing the duration
|
|
53
|
+
*/
|
|
54
|
+
export function createBar(ms, maxMs, width = 30) {
|
|
55
|
+
const ratio = Math.min(ms / maxMs, 1);
|
|
56
|
+
const filled = Math.round(ratio * width);
|
|
57
|
+
const bar = symbols.barFull.repeat(filled) + symbols.barEmpty.repeat(width - filled);
|
|
58
|
+
if (ms >= 100)
|
|
59
|
+
return ui.colors.red(bar);
|
|
60
|
+
if (ms >= 50)
|
|
61
|
+
return ui.colors.yellow(bar);
|
|
62
|
+
if (ms >= 10)
|
|
63
|
+
return ui.colors.cyan(bar);
|
|
64
|
+
return ui.colors.green(bar);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Simplifies a module URL for display:
|
|
68
|
+
* - Strips file:// protocol
|
|
69
|
+
* - Converts absolute paths to relative (using cwd)
|
|
70
|
+
* - For node_modules, shows only the package path (handles pnpm store)
|
|
71
|
+
*/
|
|
72
|
+
export function simplifyUrl(url, cwd) {
|
|
73
|
+
const withoutProtocol = url.replace(/^file:\/\//, '');
|
|
74
|
+
// Use lastIndexOf to handle pnpm store paths like:
|
|
75
|
+
// .pnpm/@pkg@version/node_modules/@scope/pkg/index.js
|
|
76
|
+
const nodeModulesIndex = withoutProtocol.lastIndexOf('node_modules/');
|
|
77
|
+
if (nodeModulesIndex !== -1) {
|
|
78
|
+
return withoutProtocol.slice(nodeModulesIndex + 'node_modules/'.length);
|
|
79
|
+
}
|
|
80
|
+
if (withoutProtocol.startsWith(cwd)) {
|
|
81
|
+
return '.' + withoutProtocol.slice(cwd.length);
|
|
82
|
+
}
|
|
83
|
+
return withoutProtocol;
|
|
84
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Reporter, ReportContext } from './base_reporter.js';
|
|
2
|
+
export declare class TuiReporter implements Reporter {
|
|
3
|
+
/**
|
|
4
|
+
* Renders an interactive TUI report using the XRay explorer.
|
|
5
|
+
* Enters alternate screen buffer for a clean experience.
|
|
6
|
+
*/
|
|
7
|
+
render(context: ReportContext): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|--------------------------------------------------------------------------
|
|
3
|
+
| TUI Reporter
|
|
4
|
+
|--------------------------------------------------------------------------
|
|
5
|
+
|
|
|
6
|
+
| Interactive terminal UI reporter using Ink.
|
|
7
|
+
| Renders an explorable dependency tree view.
|
|
8
|
+
|
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { render } from 'ink';
|
|
12
|
+
import { XRayApp } from '../../xray/components/XRayApp.js';
|
|
13
|
+
export class TuiReporter {
|
|
14
|
+
/**
|
|
15
|
+
* Renders an interactive TUI report using the XRay explorer.
|
|
16
|
+
* Enters alternate screen buffer for a clean experience.
|
|
17
|
+
*/
|
|
18
|
+
async render(context) {
|
|
19
|
+
const { result, cwd } = context;
|
|
20
|
+
// Enter alternate screen buffer
|
|
21
|
+
process.stdout.write('\x1b[?1049h');
|
|
22
|
+
process.stdout.write('\x1b[H');
|
|
23
|
+
try {
|
|
24
|
+
const { waitUntilExit } = render(React.createElement(XRayApp, { result, cwd }));
|
|
25
|
+
await waitUntilExit();
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
// Exit alternate screen buffer
|
|
29
|
+
process.stdout.write('\x1b[?1049l');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timing information for a single module import
|
|
3
|
+
*/
|
|
4
|
+
export interface ModuleTiming {
|
|
5
|
+
/**
|
|
6
|
+
* The original import specifier (e.g., '@adonisjs/core', './app/services/user.js')
|
|
7
|
+
*/
|
|
8
|
+
specifier: string;
|
|
9
|
+
/**
|
|
10
|
+
* The fully resolved URL of the module
|
|
11
|
+
*/
|
|
12
|
+
resolvedUrl: string;
|
|
13
|
+
/**
|
|
14
|
+
* Time in milliseconds to load the module (file read + parse)
|
|
15
|
+
*/
|
|
16
|
+
loadTime: number;
|
|
17
|
+
/**
|
|
18
|
+
* The URL of the parent module that imported this one
|
|
19
|
+
*/
|
|
20
|
+
parentUrl?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Total time including all transitive dependencies.
|
|
23
|
+
* This represents the actual impact of importing this module.
|
|
24
|
+
*/
|
|
25
|
+
subtreeTime?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Timing information for an AdonisJS provider
|
|
29
|
+
*/
|
|
30
|
+
export interface ProviderTiming {
|
|
31
|
+
/**
|
|
32
|
+
* Name of the provider class
|
|
33
|
+
*/
|
|
34
|
+
name: string;
|
|
35
|
+
/**
|
|
36
|
+
* Time in milliseconds for the register phase
|
|
37
|
+
*/
|
|
38
|
+
registerTime: number;
|
|
39
|
+
/**
|
|
40
|
+
* Time in milliseconds for the boot phase
|
|
41
|
+
*/
|
|
42
|
+
bootTime: number;
|
|
43
|
+
/**
|
|
44
|
+
* Time in milliseconds for the start phase
|
|
45
|
+
*/
|
|
46
|
+
startTime: number;
|
|
47
|
+
/**
|
|
48
|
+
* Time in milliseconds for the ready phase
|
|
49
|
+
*/
|
|
50
|
+
readyTime: number;
|
|
51
|
+
/**
|
|
52
|
+
* Time in milliseconds for the shutdown phase
|
|
53
|
+
*/
|
|
54
|
+
shutdownTime: number;
|
|
55
|
+
/**
|
|
56
|
+
* Total time (register + boot + start + ready)
|
|
57
|
+
*/
|
|
58
|
+
totalTime: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Complete profiling results
|
|
62
|
+
*/
|
|
63
|
+
export interface ProfileResult {
|
|
64
|
+
/**
|
|
65
|
+
* Total cold start time in milliseconds
|
|
66
|
+
*/
|
|
67
|
+
totalTime: number;
|
|
68
|
+
/**
|
|
69
|
+
* All module timing data
|
|
70
|
+
*/
|
|
71
|
+
modules: ModuleTiming[];
|
|
72
|
+
/**
|
|
73
|
+
* Provider timing data
|
|
74
|
+
*/
|
|
75
|
+
providers: ProviderTiming[];
|
|
76
|
+
/**
|
|
77
|
+
* Summary statistics
|
|
78
|
+
*/
|
|
79
|
+
summary: ProfileSummary;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* App file category types
|
|
83
|
+
*/
|
|
84
|
+
export type AppFileCategory = 'controller' | 'service' | 'model' | 'middleware' | 'validator' | 'exception' | 'event' | 'listener' | 'mailer' | 'policy' | 'command' | 'provider' | 'config' | 'start' | 'other';
|
|
85
|
+
/**
|
|
86
|
+
* Grouped app files by category
|
|
87
|
+
*/
|
|
88
|
+
export interface AppFileGroup {
|
|
89
|
+
/**
|
|
90
|
+
* Category name (e.g., 'controller', 'service')
|
|
91
|
+
*/
|
|
92
|
+
category: AppFileCategory;
|
|
93
|
+
/**
|
|
94
|
+
* Display name for the category
|
|
95
|
+
*/
|
|
96
|
+
displayName: string;
|
|
97
|
+
/**
|
|
98
|
+
* Files in this category
|
|
99
|
+
*/
|
|
100
|
+
files: ModuleTiming[];
|
|
101
|
+
/**
|
|
102
|
+
* Total load time for all files in this category
|
|
103
|
+
*/
|
|
104
|
+
totalTime: number;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Summary statistics for the profile
|
|
108
|
+
*/
|
|
109
|
+
export interface ProfileSummary {
|
|
110
|
+
/**
|
|
111
|
+
* Total number of modules loaded
|
|
112
|
+
*/
|
|
113
|
+
totalModules: number;
|
|
114
|
+
/**
|
|
115
|
+
* Number of user modules (from the app directory)
|
|
116
|
+
*/
|
|
117
|
+
userModules: number;
|
|
118
|
+
/**
|
|
119
|
+
* Number of node_modules dependencies
|
|
120
|
+
*/
|
|
121
|
+
nodeModules: number;
|
|
122
|
+
/**
|
|
123
|
+
* Number of AdonisJS core modules
|
|
124
|
+
*/
|
|
125
|
+
adonisModules: number;
|
|
126
|
+
/**
|
|
127
|
+
* Total time spent loading modules
|
|
128
|
+
*/
|
|
129
|
+
totalModuleTime: number;
|
|
130
|
+
/**
|
|
131
|
+
* Total time spent in provider lifecycle
|
|
132
|
+
*/
|
|
133
|
+
totalProviderTime: number;
|
|
134
|
+
/**
|
|
135
|
+
* App files grouped by category
|
|
136
|
+
*/
|
|
137
|
+
appFileGroups: AppFileGroup[];
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Configuration options for Docteur
|
|
141
|
+
*/
|
|
142
|
+
export interface DocteurConfig {
|
|
143
|
+
/**
|
|
144
|
+
* Number of slowest modules to display in the report
|
|
145
|
+
* @default 20
|
|
146
|
+
*/
|
|
147
|
+
topModules: number;
|
|
148
|
+
/**
|
|
149
|
+
* Only show modules that took longer than this threshold (in ms)
|
|
150
|
+
* @default 1
|
|
151
|
+
*/
|
|
152
|
+
threshold: number;
|
|
153
|
+
/**
|
|
154
|
+
* Include node_modules in the analysis
|
|
155
|
+
* @default true
|
|
156
|
+
*/
|
|
157
|
+
includeNodeModules: boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Group modules by package name
|
|
160
|
+
* @default true
|
|
161
|
+
*/
|
|
162
|
+
groupByPackage: boolean;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Resolved configuration with defaults applied
|
|
166
|
+
*/
|
|
167
|
+
export type ResolvedConfig = Required<DocteurConfig>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DependencyTree, ModuleNode } from '../tree.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
tree: DependencyTree;
|
|
4
|
+
onSelect: (node: ModuleNode) => void;
|
|
5
|
+
onSwitchToProviders: () => void;
|
|
6
|
+
hasProviders: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function ListView({ tree, onSelect, onSwitchToProviders, hasProviders }: Props): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import { formatDuration, getEffectiveTime, getFileIcon, getTimeColor, isDependency, } from '../tree.js';
|
|
5
|
+
import { symbols } from '../../profiler/registries/index.js';
|
|
6
|
+
function ItemComponent({ isSelected = false, label, timeStr, timeColor, fileIcon, displayName, }) {
|
|
7
|
+
// Spacer
|
|
8
|
+
if (!label) {
|
|
9
|
+
return _jsx(Text, { children: " " });
|
|
10
|
+
}
|
|
11
|
+
// Switch button
|
|
12
|
+
if (label.includes('Switch') || label.includes('Tab')) {
|
|
13
|
+
return (_jsx(Text, { color: isSelected ? 'blue' : 'cyan', bold: isSelected, children: label }));
|
|
14
|
+
}
|
|
15
|
+
if (isSelected) {
|
|
16
|
+
return (_jsxs(Text, { color: "blue", bold: true, children: [timeStr, " ", fileIcon, " ", displayName] }));
|
|
17
|
+
}
|
|
18
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: timeColor, children: timeStr }), " ", fileIcon, " ", displayName] }));
|
|
19
|
+
}
|
|
20
|
+
export function ListView({ tree, onSelect, onSwitchToProviders, hasProviders }) {
|
|
21
|
+
// Only show app files, not node_modules
|
|
22
|
+
const appModules = tree.sortedByTime.filter((node) => !isDependency(node.timing.resolvedUrl));
|
|
23
|
+
const moduleItems = appModules.slice(0, 30).map((node, index) => {
|
|
24
|
+
const time = getEffectiveTime(node.timing);
|
|
25
|
+
const name = node.displayName.length > 45 ? '...' + node.displayName.slice(-42) : node.displayName;
|
|
26
|
+
const url = node.timing.resolvedUrl;
|
|
27
|
+
return {
|
|
28
|
+
key: `${index}-${url}`,
|
|
29
|
+
label: name,
|
|
30
|
+
value: node,
|
|
31
|
+
timeStr: formatDuration(time).padStart(10),
|
|
32
|
+
timeColor: getTimeColor(time),
|
|
33
|
+
fileIcon: getFileIcon(url),
|
|
34
|
+
displayName: name,
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
const items = hasProviders
|
|
38
|
+
? [
|
|
39
|
+
...moduleItems,
|
|
40
|
+
{
|
|
41
|
+
key: 'spacer',
|
|
42
|
+
label: '',
|
|
43
|
+
value: 'switch',
|
|
44
|
+
timeStr: '',
|
|
45
|
+
timeColor: 'green',
|
|
46
|
+
fileIcon: '',
|
|
47
|
+
displayName: '',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: 'switch',
|
|
51
|
+
label: `${symbols.lightning} Switch to Providers (Tab)`,
|
|
52
|
+
value: 'switch',
|
|
53
|
+
timeStr: '',
|
|
54
|
+
timeColor: 'green',
|
|
55
|
+
fileIcon: '',
|
|
56
|
+
displayName: '',
|
|
57
|
+
},
|
|
58
|
+
]
|
|
59
|
+
: moduleItems;
|
|
60
|
+
const handleSelect = (item) => {
|
|
61
|
+
if (item.value === 'switch') {
|
|
62
|
+
onSwitchToProviders();
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
onSelect(item.value);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: [' ', '\ue234', " Docteur X-Ray - Module Explorer"] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: " Use arrows to navigate, Enter to inspect, Tab to switch views" }) }), _jsx(Box, { flexDirection: "column", children: _jsx(SelectInput, { items: items, onSelect: handleSelect, itemComponent: ItemComponent }) })] }));
|
|
69
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DependencyTree, ModuleNode } from '../tree.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
node: ModuleNode;
|
|
4
|
+
tree: DependencyTree;
|
|
5
|
+
onNavigate: (node: ModuleNode) => void;
|
|
6
|
+
onBack: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function ModuleView({ node, tree: _tree, onNavigate, onBack }: Props): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|