@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.
Files changed (47) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +155 -0
  3. package/build/index.d.ts +23 -0
  4. package/build/index.js +23 -0
  5. package/build/src/cli/commands/diagnose.d.ts +27 -0
  6. package/build/src/cli/commands/diagnose.js +67 -0
  7. package/build/src/cli/commands/xray.d.ts +7 -0
  8. package/build/src/cli/commands/xray.js +49 -0
  9. package/build/src/cli/main.d.ts +2 -0
  10. package/build/src/cli/main.js +30 -0
  11. package/build/src/profiler/collector.d.ts +16 -0
  12. package/build/src/profiler/collector.js +142 -0
  13. package/build/src/profiler/hooks.d.ts +27 -0
  14. package/build/src/profiler/hooks.js +45 -0
  15. package/build/src/profiler/loader.d.ts +1 -0
  16. package/build/src/profiler/loader.js +111 -0
  17. package/build/src/profiler/profiler.d.ts +9 -0
  18. package/build/src/profiler/profiler.js +117 -0
  19. package/build/src/profiler/registries/categories.d.ts +7 -0
  20. package/build/src/profiler/registries/categories.js +87 -0
  21. package/build/src/profiler/registries/index.d.ts +2 -0
  22. package/build/src/profiler/registries/index.js +2 -0
  23. package/build/src/profiler/registries/symbols.d.ts +42 -0
  24. package/build/src/profiler/registries/symbols.js +71 -0
  25. package/build/src/profiler/reporters/base_reporter.d.ts +19 -0
  26. package/build/src/profiler/reporters/base_reporter.js +9 -0
  27. package/build/src/profiler/reporters/console_reporter.d.ts +8 -0
  28. package/build/src/profiler/reporters/console_reporter.js +237 -0
  29. package/build/src/profiler/reporters/format.d.ts +62 -0
  30. package/build/src/profiler/reporters/format.js +84 -0
  31. package/build/src/profiler/reporters/tui_reporter.d.ts +8 -0
  32. package/build/src/profiler/reporters/tui_reporter.js +32 -0
  33. package/build/src/types.d.ts +167 -0
  34. package/build/src/types.js +9 -0
  35. package/build/src/xray/components/ListView.d.ts +9 -0
  36. package/build/src/xray/components/ListView.js +69 -0
  37. package/build/src/xray/components/ModuleView.d.ts +9 -0
  38. package/build/src/xray/components/ModuleView.js +144 -0
  39. package/build/src/xray/components/ProviderListView.d.ts +8 -0
  40. package/build/src/xray/components/ProviderListView.js +57 -0
  41. package/build/src/xray/components/ProviderView.d.ts +7 -0
  42. package/build/src/xray/components/ProviderView.js +35 -0
  43. package/build/src/xray/components/XRayApp.d.ts +7 -0
  44. package/build/src/xray/components/XRayApp.js +78 -0
  45. package/build/src/xray/tree.d.ts +22 -0
  46. package/build/src/xray/tree.js +85 -0
  47. package/package.json +110 -0
@@ -0,0 +1,144 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import SelectInput from 'ink-select-input';
5
+ import { formatDuration, getEffectiveTime, getImportChain, getFileIcon, getSourceIcon, getTimeColor, isDependency, } from '../tree.js';
6
+ import { symbols } from '../../profiler/registries/index.js';
7
+ function ItemComponent({ isSelected = false, label }) {
8
+ if (!label) {
9
+ return _jsx(Text, { children: " " });
10
+ }
11
+ return (_jsx(Text, { color: isSelected ? 'blue' : undefined, bold: isSelected, children: label }));
12
+ }
13
+ // Threshold for suggesting lazy imports (50ms)
14
+ const LAZY_IMPORT_THRESHOLD = 50;
15
+ export function ModuleView({ node, tree: _tree, onNavigate, onBack }) {
16
+ const importChain = useMemo(() => getImportChain(node), [node]);
17
+ const time = getEffectiveTime(node.timing);
18
+ const selfTime = node.timing.loadTime;
19
+ // Find heavy dependencies that could be lazy loaded
20
+ const lazyImportCandidates = useMemo(() => {
21
+ return node.children
22
+ .filter((child) => {
23
+ const childTime = getEffectiveTime(child.timing);
24
+ return childTime >= LAZY_IMPORT_THRESHOLD && isDependency(child.timing.resolvedUrl);
25
+ })
26
+ .sort((a, b) => getEffectiveTime(b.timing) - getEffectiveTime(a.timing));
27
+ }, [node.children]);
28
+ // Build navigable items list
29
+ const items = useMemo(() => {
30
+ const result = [];
31
+ result.push({
32
+ key: 'back',
33
+ label: `${symbols.arrowLeft} Back`,
34
+ value: 'back',
35
+ });
36
+ // Lazy import recommendations
37
+ if (lazyImportCandidates.length > 0 && !isDependency(node.timing.resolvedUrl)) {
38
+ result.push({
39
+ key: 'sep-lazy',
40
+ label: `--- ${symbols.lightbulb} Lazy Import Candidates ---`,
41
+ value: 'back',
42
+ });
43
+ for (const candidate of lazyImportCandidates.slice(0, 5)) {
44
+ const childTime = getEffectiveTime(candidate.timing);
45
+ const pkgName = extractPackageName(candidate.timing.resolvedUrl);
46
+ result.push({
47
+ key: `lazy-${candidate.timing.resolvedUrl}`,
48
+ label: ` ${symbols.arrow} ${pkgName} (${formatDuration(childTime)}) - use dynamic import`,
49
+ value: candidate,
50
+ });
51
+ }
52
+ }
53
+ // Import chain (why loaded)
54
+ if (importChain.length > 1) {
55
+ result.push({
56
+ key: 'sep-why',
57
+ label: '--- Why was this loaded? ---',
58
+ value: 'back',
59
+ });
60
+ for (let i = 0; i < importChain.length - 1; i++) {
61
+ const chainNode = importChain[i];
62
+ const indent = ' '.repeat(i);
63
+ const chainTime = getEffectiveTime(chainNode.timing);
64
+ const fileIcon = getFileIcon(chainNode.timing.resolvedUrl);
65
+ const sourceIcon = getSourceIcon(chainNode.timing.resolvedUrl);
66
+ const isEntry = i === 0;
67
+ result.push({
68
+ key: `chain-${i}-${chainNode.timing.resolvedUrl}`,
69
+ label: `${indent}${isEntry ? '\u25B6' : '\u2514\u2500'} ${sourceIcon} ${fileIcon} ${chainNode.displayName} (${formatDuration(chainTime)})`,
70
+ value: chainNode,
71
+ });
72
+ }
73
+ const currentIndent = ' '.repeat(importChain.length - 1);
74
+ const currentFileIcon = getFileIcon(node.timing.resolvedUrl);
75
+ const currentSourceIcon = getSourceIcon(node.timing.resolvedUrl);
76
+ result.push({
77
+ key: 'current',
78
+ label: `${currentIndent}\u2514\u2500 ${currentSourceIcon} ${currentFileIcon} ${node.displayName} ${symbols.arrowLeft} YOU ARE HERE`,
79
+ value: 'back',
80
+ });
81
+ }
82
+ // Children (what it imports)
83
+ if (node.children.length > 0) {
84
+ const sortedChildren = [...node.children].sort((a, b) => getEffectiveTime(b.timing) - getEffectiveTime(a.timing));
85
+ result.push({
86
+ key: 'spacer-imports',
87
+ label: '',
88
+ value: 'back',
89
+ });
90
+ result.push({
91
+ key: 'sep-imports',
92
+ label: `--- What it imports (${node.children.length} modules) ---`,
93
+ value: 'back',
94
+ });
95
+ for (let i = 0; i < Math.min(sortedChildren.length, 15); i++) {
96
+ const child = sortedChildren[i];
97
+ const childTime = getEffectiveTime(child.timing);
98
+ const childFileIcon = getFileIcon(child.timing.resolvedUrl);
99
+ const childSourceIcon = getSourceIcon(child.timing.resolvedUrl);
100
+ const isHeavy = childTime >= LAZY_IMPORT_THRESHOLD && isDependency(child.timing.resolvedUrl);
101
+ const childName = simplifyDisplayName(child);
102
+ result.push({
103
+ key: `child-${i}-${child.timing.resolvedUrl}`,
104
+ label: ` ${isHeavy ? symbols.warning : '\u25B8'} ${childSourceIcon} ${childFileIcon} ${childName} (${formatDuration(childTime)})`,
105
+ value: child,
106
+ });
107
+ }
108
+ if (sortedChildren.length > 15) {
109
+ result.push({
110
+ key: 'more',
111
+ label: ` ... and ${sortedChildren.length - 15} more`,
112
+ value: 'back',
113
+ });
114
+ }
115
+ }
116
+ return result;
117
+ }, [node, importChain, lazyImportCandidates]);
118
+ const handleSelect = (item) => {
119
+ if (item.value === 'back') {
120
+ if (item.label.includes('Back')) {
121
+ onBack();
122
+ }
123
+ return;
124
+ }
125
+ onNavigate(item.value);
126
+ };
127
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: [' ', '\uf21e', " Module Details"] }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { children: _jsxs(Text, { bold: true, color: "green", children: [' ', node.displayName] }) }), _jsxs(Text, { dimColor: true, children: [" ", node.timing.resolvedUrl] })] }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " Total time: " }), _jsx(Text, { color: getTimeColor(time), children: formatDuration(time) }), _jsx(Text, { dimColor: true, children: " (with dependencies)" })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " Self time: " }), _jsx(Text, { color: getTimeColor(selfTime), children: formatDuration(selfTime) }), _jsx(Text, { dimColor: true, children: " (this file only)" })] })] }), lazyImportCandidates.length > 0 && !isDependency(node.timing.resolvedUrl) && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", paddingLeft: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: [symbols.lightbulb, " Optimization tip:"] }), _jsxs(Text, { dimColor: true, children: [' ', "This file imports ", lazyImportCandidates.length, " heavy package(s) that could be"] }), _jsx(Text, { dimColor: true, children: " lazy-loaded with dynamic imports to reduce cold start time:" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: " Before: import xlsx from 'xlsx'" }), _jsx(Text, { dimColor: true, children: " After: const xlsx = await import('xlsx')" })] })] })), _jsx(Box, { flexDirection: "column", children: _jsx(SelectInput, { items: items, onSelect: handleSelect, itemComponent: ItemComponent }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: " Left/ESC/Backspace: back | q: quit" }) })] }));
128
+ }
129
+ function extractPackageName(url) {
130
+ const match = url.match(/node_modules\/(@[^/]+\/[^/]+|[^/]+)/);
131
+ return match?.[1] || url.split('/').pop() || url;
132
+ }
133
+ function simplifyDisplayName(node) {
134
+ const url = node.timing.resolvedUrl;
135
+ if (isDependency(url)) {
136
+ // Find the LAST node_modules/ to handle pnpm store paths
137
+ // e.g. .pnpm/@pkg@version/node_modules/@scope/pkg/build/index.js
138
+ const lastIdx = url.lastIndexOf('node_modules/');
139
+ if (lastIdx !== -1) {
140
+ return url.slice(lastIdx + 'node_modules/'.length);
141
+ }
142
+ }
143
+ return node.displayName;
144
+ }
@@ -0,0 +1,8 @@
1
+ import type { ProviderTiming } from '../../types.js';
2
+ interface Props {
3
+ providers: ProviderTiming[];
4
+ onSelect: (provider: ProviderTiming) => void;
5
+ onSwitchToModules: () => void;
6
+ }
7
+ export declare function ProviderListView({ providers, onSelect, onSwitchToModules }: Props): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,57 @@
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, getTimeColor } from '../tree.js';
5
+ import { symbols } from '../../profiler/registries/index.js';
6
+ function ItemComponent({ isSelected = false, timeStr, timeColor, name, label }) {
7
+ // Spacer
8
+ if (!label) {
9
+ return _jsx(Text, { children: " " });
10
+ }
11
+ // Switch button
12
+ if (label.includes('Switch')) {
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, " ", symbols.provider, " ", name] }));
17
+ }
18
+ return (_jsxs(Text, { children: [_jsx(Text, { color: timeColor, children: timeStr }), " ", symbols.provider, " ", name] }));
19
+ }
20
+ export function ProviderListView({ providers, onSelect, onSwitchToModules }) {
21
+ const sorted = [...providers].sort((a, b) => b.totalTime - a.totalTime);
22
+ const items = [
23
+ ...sorted.map((provider, index) => ({
24
+ key: `${index}-${provider.name}`,
25
+ label: provider.name,
26
+ value: provider,
27
+ timeStr: formatDuration(provider.totalTime).padStart(10),
28
+ timeColor: getTimeColor(provider.totalTime),
29
+ name: provider.name,
30
+ })),
31
+ {
32
+ key: 'spacer',
33
+ label: '',
34
+ value: 'switch',
35
+ timeStr: '',
36
+ timeColor: 'green',
37
+ name: '',
38
+ },
39
+ {
40
+ key: 'switch',
41
+ label: `${symbols.turtle} Switch to Modules (Tab)`,
42
+ value: 'switch',
43
+ timeStr: '',
44
+ timeColor: 'green',
45
+ name: '',
46
+ },
47
+ ];
48
+ const handleSelect = (item) => {
49
+ if (item.value === 'switch') {
50
+ onSwitchToModules();
51
+ }
52
+ else {
53
+ onSelect(item.value);
54
+ }
55
+ };
56
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: [' ', symbols.lightning, " Docteur X-Ray - Provider Explorer"] }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [' ', "Time spent in register() + boot() + start() + ready() lifecycle methods"] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: " Use arrows to navigate, Enter to inspect, ESC to quit" }) }), _jsx(Box, { flexDirection: "column", children: _jsx(SelectInput, { items: items, onSelect: handleSelect, itemComponent: ItemComponent }) })] }));
57
+ }
@@ -0,0 +1,7 @@
1
+ import type { ProviderTiming } from '../../types.js';
2
+ interface Props {
3
+ provider: ProviderTiming;
4
+ onBack: () => void;
5
+ }
6
+ export declare function ProviderView({ provider, onBack }: Props): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,35 @@
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, getTimeColor } from '../tree.js';
5
+ import { symbols } from '../../profiler/registries/index.js';
6
+ function Bar({ value, max, width = 30 }) {
7
+ const ratio = max > 0 ? Math.min(value / max, 1) : 0;
8
+ const filled = Math.round(ratio * width);
9
+ const bar = symbols.barFull.repeat(filled) + symbols.barEmpty.repeat(width - filled);
10
+ const color = getTimeColor(value);
11
+ return _jsx(Text, { color: color, children: bar });
12
+ }
13
+ function PhaseRow({ label, value, max }) {
14
+ const timeStr = formatDuration(value).padStart(10);
15
+ const color = getTimeColor(value);
16
+ return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: label.padEnd(12) }), _jsx(Text, { color: color, children: timeStr }), _jsx(Text, { children: " " }), _jsx(Bar, { value: value, max: max, width: 25 })] }));
17
+ }
18
+ export function ProviderView({ provider, onBack }) {
19
+ const phases = [
20
+ { label: 'register()', value: provider.registerTime },
21
+ { label: 'boot()', value: provider.bootTime },
22
+ { label: 'start()', value: provider.startTime },
23
+ { label: 'ready()', value: provider.readyTime },
24
+ ];
25
+ const maxPhaseTime = Math.max(...phases.map((p) => p.value), 1);
26
+ const items = [
27
+ { key: 'back', label: `${symbols.arrowLeft} Back`, value: 'back' },
28
+ ];
29
+ const handleSelect = (item) => {
30
+ if (item.value === 'back') {
31
+ onBack();
32
+ }
33
+ };
34
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: [' ', symbols.lightning, " Provider Details"] }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { children: _jsxs(Text, { bold: true, color: "green", children: [' ', symbols.provider, " ", provider.name] }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " Total execution time: " }), _jsx(Text, { color: getTimeColor(provider.totalTime), children: formatDuration(provider.totalTime) })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, children: " Lifecycle Breakdown:" }), _jsx(Text, { dimColor: true, children: " Time spent in each provider method" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: phases.map((phase) => (_jsx(PhaseRow, { label: phase.label, value: phase.value, max: maxPhaseTime }, phase.label))) })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 1, children: [_jsxs(Text, { dimColor: true, children: [symbols.bullet, " register(): Container bindings, executed for all providers first"] }), _jsxs(Text, { dimColor: true, children: [symbols.bullet, " boot(): Initialization logic, after all providers registered"] }), _jsxs(Text, { dimColor: true, children: [symbols.bullet, " start(): Called when HTTP server starts"] }), _jsxs(Text, { dimColor: true, children: [symbols.bullet, " ready(): Called when app is fully ready"] })] }), _jsx(Box, { flexDirection: "column", children: _jsx(SelectInput, { items: items, onSelect: handleSelect }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: " Left/ESC/Backspace: back | q: quit" }) })] }));
35
+ }
@@ -0,0 +1,7 @@
1
+ import type { ProfileResult } from '../../types.js';
2
+ interface Props {
3
+ result: ProfileResult;
4
+ cwd: string;
5
+ }
6
+ export declare function XRayApp({ result, cwd }: Props): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,78 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState, useMemo, useCallback } from 'react';
3
+ import { useApp, useInput, useStdout } from 'ink';
4
+ import { buildDependencyTree } from '../tree.js';
5
+ import { ListView } from './ListView.js';
6
+ import { ModuleView } from './ModuleView.js';
7
+ import { ProviderListView } from './ProviderListView.js';
8
+ import { ProviderView } from './ProviderView.js';
9
+ export function XRayApp({ result, cwd }) {
10
+ const { exit } = useApp();
11
+ const { write } = useStdout();
12
+ const [view, setView] = useState('modules');
13
+ const [moduleHistory, setModuleHistory] = useState([]);
14
+ const [selectedProvider, setSelectedProvider] = useState(null);
15
+ const tree = useMemo(() => buildDependencyTree(result.modules, cwd), [result.modules, cwd]);
16
+ const currentModule = moduleHistory.length > 0 ? moduleHistory[moduleHistory.length - 1] : null;
17
+ const clearScreen = useCallback(() => {
18
+ write('\x1b[2J\x1b[H');
19
+ }, [write]);
20
+ const navigateToModule = (node) => {
21
+ clearScreen();
22
+ setModuleHistory([...moduleHistory, node]);
23
+ };
24
+ const goBack = () => {
25
+ clearScreen();
26
+ if (selectedProvider) {
27
+ setSelectedProvider(null);
28
+ }
29
+ else if (moduleHistory.length > 0) {
30
+ setModuleHistory(moduleHistory.slice(0, -1));
31
+ }
32
+ };
33
+ const switchView = (newView) => {
34
+ clearScreen();
35
+ setView(newView);
36
+ setModuleHistory([]);
37
+ setSelectedProvider(null);
38
+ };
39
+ useInput((input, key) => {
40
+ if (input === 'q') {
41
+ exit();
42
+ }
43
+ if (key.leftArrow || key.backspace || key.delete) {
44
+ if (selectedProvider || moduleHistory.length > 0) {
45
+ goBack();
46
+ }
47
+ }
48
+ if (key.escape) {
49
+ if (selectedProvider || moduleHistory.length > 0) {
50
+ goBack();
51
+ }
52
+ else {
53
+ exit();
54
+ }
55
+ }
56
+ // Tab to switch between views (when at root)
57
+ if (key.tab && !currentModule && !selectedProvider) {
58
+ switchView(view === 'modules' ? 'providers' : 'modules');
59
+ }
60
+ });
61
+ // Provider detail view
62
+ if (selectedProvider) {
63
+ return _jsx(ProviderView, { provider: selectedProvider, onBack: goBack });
64
+ }
65
+ // Module detail view
66
+ if (currentModule) {
67
+ return (_jsx(ModuleView, { node: currentModule, tree: tree, onNavigate: navigateToModule, onBack: goBack }));
68
+ }
69
+ // Providers list view
70
+ if (view === 'providers') {
71
+ return (_jsx(ProviderListView, { providers: result.providers, onSelect: (p) => {
72
+ clearScreen();
73
+ setSelectedProvider(p);
74
+ }, onSwitchToModules: () => switchView('modules') }));
75
+ }
76
+ // Modules list view (default)
77
+ return (_jsx(ListView, { tree: tree, onSelect: navigateToModule, onSwitchToProviders: () => switchView('providers'), hasProviders: result.providers.length > 0 }));
78
+ }
@@ -0,0 +1,22 @@
1
+ import type { ModuleTiming } from '../types.js';
2
+ import { formatDuration, getEffectiveTime } from '../profiler/reporters/format.js';
3
+ export interface ModuleNode {
4
+ timing: ModuleTiming;
5
+ displayName: string;
6
+ children: ModuleNode[];
7
+ parent?: ModuleNode;
8
+ depth: number;
9
+ }
10
+ export interface DependencyTree {
11
+ nodeMap: Map<string, ModuleNode>;
12
+ roots: ModuleNode[];
13
+ sortedByTime: ModuleNode[];
14
+ }
15
+ export declare function buildDependencyTree(modules: ModuleTiming[], cwd: string): DependencyTree;
16
+ export declare function getImportChain(node: ModuleNode): ModuleNode[];
17
+ export declare function getFileIcon(url: string): string;
18
+ export type TimeColor = 'red' | 'yellow' | 'cyan' | 'green';
19
+ export declare function getTimeColor(ms: number): TimeColor;
20
+ export declare function isDependency(url: string): boolean;
21
+ export declare function getSourceIcon(url: string): string;
22
+ export { formatDuration, getEffectiveTime };
@@ -0,0 +1,85 @@
1
+ /*
2
+ |--------------------------------------------------------------------------
3
+ | Dependency Tree
4
+ |--------------------------------------------------------------------------
5
+ |
6
+ | Builds a tree structure from module timings for the xray TUI.
7
+ | Provides utilities for traversing and displaying the tree.
8
+ |
9
+ */
10
+ import { formatDuration, getEffectiveTime, simplifyUrl } from '../profiler/reporters/format.js';
11
+ import { fileIcons, symbols } from '../profiler/registries/index.js';
12
+ export function buildDependencyTree(modules, cwd) {
13
+ const nodeMap = new Map();
14
+ const time = (n) => getEffectiveTime(n.timing);
15
+ // Create all nodes
16
+ for (const timing of modules) {
17
+ nodeMap.set(timing.resolvedUrl, {
18
+ timing,
19
+ displayName: simplifyUrl(timing.resolvedUrl, cwd),
20
+ children: [],
21
+ depth: 0,
22
+ });
23
+ }
24
+ for (const node of nodeMap.values()) {
25
+ const parentUrl = node.timing.parentUrl;
26
+ if (parentUrl && nodeMap.has(parentUrl)) {
27
+ const parent = nodeMap.get(parentUrl);
28
+ parent.children.push(node);
29
+ node.parent = parent;
30
+ }
31
+ }
32
+ // Calculate depths and identify roots
33
+ const roots = [];
34
+ const setDepths = (node, depth, seen = new Set()) => {
35
+ if (seen.has(node))
36
+ return;
37
+ seen.add(node);
38
+ node.depth = depth;
39
+ for (const child of node.children)
40
+ setDepths(child, depth + 1, seen);
41
+ };
42
+ for (const node of nodeMap.values()) {
43
+ if (!node.parent) {
44
+ roots.push(node);
45
+ setDepths(node, 0);
46
+ }
47
+ }
48
+ // Sort children by time (slowest first)
49
+ for (const node of nodeMap.values()) {
50
+ node.children.sort((a, b) => time(b) - time(a));
51
+ }
52
+ const sortedByTime = [...nodeMap.values()].sort((a, b) => time(b) - time(a));
53
+ return { nodeMap, roots, sortedByTime };
54
+ }
55
+ export function getImportChain(node) {
56
+ const chain = [];
57
+ const seen = new Set();
58
+ let current = node;
59
+ while (current && !seen.has(current)) {
60
+ seen.add(current);
61
+ chain.unshift(current);
62
+ current = current.parent;
63
+ }
64
+ return chain;
65
+ }
66
+ export function getFileIcon(url) {
67
+ const ext = url.split('.').pop()?.toLowerCase() || '';
68
+ return fileIcons[ext] || fileIcons.default;
69
+ }
70
+ export function getTimeColor(ms) {
71
+ if (ms >= 100)
72
+ return 'red';
73
+ if (ms >= 50)
74
+ return 'yellow';
75
+ if (ms >= 10)
76
+ return 'cyan';
77
+ return 'green';
78
+ }
79
+ export function isDependency(url) {
80
+ return url.includes('/node_modules/');
81
+ }
82
+ export function getSourceIcon(url) {
83
+ return isDependency(url) ? symbols.sourcePackage : symbols.sourceHome;
84
+ }
85
+ export { formatDuration, getEffectiveTime };
package/package.json ADDED
@@ -0,0 +1,110 @@
1
+ {
2
+ "name": "@densetsuuu/docteur",
3
+ "description": "AdonisJS cold start profiler - analyze and optimize your application boot time",
4
+ "version": "0.1.1-beta",
5
+ "engines": {
6
+ "node": ">=21.0.0"
7
+ },
8
+ "main": "./build/index.js",
9
+ "types": "./build/index.d.ts",
10
+ "type": "module",
11
+ "bin": {
12
+ "docteur": "./build/src/cli/main.js"
13
+ },
14
+ "files": [
15
+ "build/src",
16
+ "build/index.d.ts",
17
+ "build/index.js"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "types": "./build/index.d.ts",
22
+ "import": "./build/index.js"
23
+ },
24
+ "./types": "./build/src/types.js",
25
+ "./profiler/loader": "./build/src/profiler/loader.js",
26
+ "./profiler/hooks": "./build/src/profiler/hooks.js"
27
+ },
28
+ "scripts": {
29
+ "clean": "del-cli build",
30
+ "typecheck": "tsc --noEmit",
31
+ "lint": "eslint .",
32
+ "format": "prettier --write .",
33
+ "quick:test": "node --import=./tsnode.esm.js --enable-source-maps bin/test.ts",
34
+ "pretest": "npm run lint",
35
+ "test": "c8 npm run quick:test",
36
+ "prebuild": "npm run lint && npm run clean",
37
+ "build": "tsc",
38
+ "release": "np",
39
+ "version": "npm run build",
40
+ "prepublishOnly": "npm run build"
41
+ },
42
+ "keywords": [
43
+ "adonisjs",
44
+ "adonis",
45
+ "profiler",
46
+ "cold-start",
47
+ "performance",
48
+ "diagnostics",
49
+ "boot-time",
50
+ "cli"
51
+ ],
52
+ "author": "Densetsuuu",
53
+ "license": "MIT",
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/densetsuuu/docteur.git"
57
+ },
58
+ "bugs": {
59
+ "url": "https://github.com/densetsuuu/docteur/issues"
60
+ },
61
+ "homepage": "https://github.com/densetsuuu/docteur#readme",
62
+ "dependencies": {
63
+ "@poppinss/cliui": "^6.6.0",
64
+ "citty": "^0.2.0",
65
+ "ink": "^5.2.1",
66
+ "ink-select-input": "^6.2.0",
67
+ "react": "^18.3.1"
68
+ },
69
+ "devDependencies": {
70
+ "@adonisjs/application": "9.0.0-next.15",
71
+ "@adonisjs/eslint-config": "^3.0.0-next.9",
72
+ "@adonisjs/prettier-config": "^1.4.5",
73
+ "@adonisjs/tsconfig": "^2.0.0-next.3",
74
+ "@japa/assert": "^4.2.0",
75
+ "@japa/runner": "^5.0.0",
76
+ "@swc/core": "^1.6.3",
77
+ "@types/node": "^25.0.3",
78
+ "@types/react": "^18.3.27",
79
+ "c8": "^10.1.2",
80
+ "del-cli": "^5.1.0",
81
+ "eslint": "^9.15.0",
82
+ "np": "^10.0.6",
83
+ "prettier": "^3.3.2",
84
+ "ts-node-maintained": "^10.9.4",
85
+ "typescript": "^5.4.5"
86
+ },
87
+ "peerDependencies": {
88
+ "@adonisjs/application": ">=9.0.0"
89
+ },
90
+ "publishConfig": {
91
+ "access": "public",
92
+ "tag": "latest"
93
+ },
94
+ "np": {
95
+ "message": "chore(release): %s",
96
+ "tag": "latest",
97
+ "branch": "main",
98
+ "anyBranch": false
99
+ },
100
+ "c8": {
101
+ "reporter": [
102
+ "text",
103
+ "html"
104
+ ],
105
+ "exclude": [
106
+ "tests/**"
107
+ ]
108
+ },
109
+ "prettier": "@adonisjs/prettier-config"
110
+ }