@emeryld/manager 1.5.1 → 1.5.2
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/dist/publish.js +26 -2
- package/dist/ts-trace/cli/options.js +196 -0
- package/dist/ts-trace/cli/prompts.js +148 -0
- package/dist/ts-trace/cli/settings.js +154 -0
- package/dist/ts-trace/config.js +100 -0
- package/dist/ts-trace/generate.js +150 -0
- package/dist/ts-trace/index.js +137 -0
- package/dist/ts-trace/picker.js +373 -0
- package/dist/ts-trace/report.js +718 -0
- package/dist/ts-trace/types.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { rootDir } from '../helper-cli/env.js';
|
|
6
|
+
import { colors, logGlobal } from '../utils/log.js';
|
|
7
|
+
import { run } from '../utils/run.js';
|
|
8
|
+
import { TraceReportError } from './report.js';
|
|
9
|
+
const PROJECT_CONFIG_CANDIDATES = [
|
|
10
|
+
'tsconfig.typecheck.json',
|
|
11
|
+
'tsconfig.base.json',
|
|
12
|
+
'tsconfig.json',
|
|
13
|
+
];
|
|
14
|
+
const TEMP_CONFIG_DIR = path.join(rootDir, '.manager-tmp', 'ts-trace');
|
|
15
|
+
const TRACE_FILE_NAME = 'trace.json';
|
|
16
|
+
const TYPES_FILE_NAME = 'types.json';
|
|
17
|
+
export async function resolveTraceProfileTargetFromPath(rawPath) {
|
|
18
|
+
const trimmed = rawPath.trim();
|
|
19
|
+
if (!trimmed) {
|
|
20
|
+
throw new TraceReportError('Target path cannot be empty.', 2);
|
|
21
|
+
}
|
|
22
|
+
const absolutePath = path.isAbsolute(trimmed)
|
|
23
|
+
? path.resolve(trimmed)
|
|
24
|
+
: path.resolve(rootDir, trimmed);
|
|
25
|
+
const stats = await stat(absolutePath).catch(() => undefined);
|
|
26
|
+
if (!stats) {
|
|
27
|
+
throw new TraceReportError(`Target path not found: ${absolutePath}`, 2);
|
|
28
|
+
}
|
|
29
|
+
if (!stats.isFile() && !stats.isDirectory()) {
|
|
30
|
+
throw new TraceReportError(`Target must be a file or directory: ${absolutePath}`, 2);
|
|
31
|
+
}
|
|
32
|
+
if (!isPathInWorkspace(absolutePath)) {
|
|
33
|
+
throw new TraceReportError(`Target must be inside this workspace: ${absolutePath}`, 2);
|
|
34
|
+
}
|
|
35
|
+
const kind = stats.isDirectory() ? 'directory' : 'file';
|
|
36
|
+
return {
|
|
37
|
+
absolutePath,
|
|
38
|
+
kind,
|
|
39
|
+
label: formatWorkspaceRelativePath(absolutePath),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export async function generateTypeScriptTraceForTarget(options) {
|
|
43
|
+
const projectPath = await resolveProjectConfigPath(options.projectPathSetting);
|
|
44
|
+
const traceName = buildTraceName(options.target.label);
|
|
45
|
+
const traceDir = path.join(tmpdir(), `ts-trace-${traceName}-${Date.now()}`);
|
|
46
|
+
const tempConfigPath = await writeScopedTraceConfig(options.target, projectPath);
|
|
47
|
+
try {
|
|
48
|
+
logGlobal(`Generating trace for ${options.target.label} using ${formatWorkspaceRelativePath(projectPath)}`, colors.cyan);
|
|
49
|
+
await run('pnpm', [
|
|
50
|
+
'tsc',
|
|
51
|
+
'-p',
|
|
52
|
+
tempConfigPath,
|
|
53
|
+
'--incremental',
|
|
54
|
+
'false',
|
|
55
|
+
'--generateTrace',
|
|
56
|
+
traceDir,
|
|
57
|
+
'--pretty',
|
|
58
|
+
'false',
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const hasArtifacts = await traceArtifactsExist(traceDir);
|
|
63
|
+
if (!hasArtifacts) {
|
|
64
|
+
throw new TraceReportError(`Failed to generate a trace for ${options.target.label}. ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
65
|
+
}
|
|
66
|
+
logGlobal('TypeScript reported errors but emitted trace artifacts; continuing analysis.', colors.yellow);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await rm(tempConfigPath, { force: true }).catch(() => { });
|
|
70
|
+
}
|
|
71
|
+
const hasArtifacts = await traceArtifactsExist(traceDir);
|
|
72
|
+
if (!hasArtifacts) {
|
|
73
|
+
throw new TraceReportError(`Trace generation finished without ${TRACE_FILE_NAME} and ${TYPES_FILE_NAME}: ${traceDir}`, 1);
|
|
74
|
+
}
|
|
75
|
+
return { traceDir, projectPath };
|
|
76
|
+
}
|
|
77
|
+
export async function resolveProjectConfigPath(projectPathSetting) {
|
|
78
|
+
const configured = projectPathSetting?.trim();
|
|
79
|
+
if (configured && configured.toLowerCase() !== 'auto') {
|
|
80
|
+
const resolved = path.isAbsolute(configured)
|
|
81
|
+
? path.resolve(configured)
|
|
82
|
+
: path.resolve(rootDir, configured);
|
|
83
|
+
const stats = await stat(resolved).catch(() => undefined);
|
|
84
|
+
if (!stats?.isFile()) {
|
|
85
|
+
throw new TraceReportError(`Configured TypeScript project not found: ${resolved}`, 2);
|
|
86
|
+
}
|
|
87
|
+
return resolved;
|
|
88
|
+
}
|
|
89
|
+
for (const candidate of PROJECT_CONFIG_CANDIDATES) {
|
|
90
|
+
const candidatePath = path.join(rootDir, candidate);
|
|
91
|
+
if (!existsSync(candidatePath))
|
|
92
|
+
continue;
|
|
93
|
+
return candidatePath;
|
|
94
|
+
}
|
|
95
|
+
throw new TraceReportError(`Could not find a TypeScript project config. Checked: ${PROJECT_CONFIG_CANDIDATES.join(', ')}`, 2);
|
|
96
|
+
}
|
|
97
|
+
async function writeScopedTraceConfig(target, projectPath) {
|
|
98
|
+
await mkdir(TEMP_CONFIG_DIR, { recursive: true });
|
|
99
|
+
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
100
|
+
const tempConfigPath = path.join(TEMP_CONFIG_DIR, `tsconfig.${suffix}.json`);
|
|
101
|
+
const extendsPath = toPosixPath(path.relative(path.dirname(tempConfigPath), projectPath));
|
|
102
|
+
const relativeTargetPath = toPosixPath(path.relative(path.dirname(tempConfigPath), target.absolutePath));
|
|
103
|
+
const includePattern = buildIncludePattern(relativeTargetPath, target.kind);
|
|
104
|
+
const payload = {
|
|
105
|
+
extends: extendsPath || './tsconfig.json',
|
|
106
|
+
include: [includePattern],
|
|
107
|
+
compilerOptions: {
|
|
108
|
+
noEmit: true,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
await writeFile(tempConfigPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
112
|
+
return tempConfigPath;
|
|
113
|
+
}
|
|
114
|
+
function buildIncludePattern(relativePath, kind) {
|
|
115
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
116
|
+
if (!normalized || normalized === '.') {
|
|
117
|
+
return kind === 'directory' ? '**/*' : normalized;
|
|
118
|
+
}
|
|
119
|
+
return kind === 'directory' ? `${normalized}/**/*` : normalized;
|
|
120
|
+
}
|
|
121
|
+
function buildTraceName(label) {
|
|
122
|
+
const sanitized = label
|
|
123
|
+
.replace(/\\/g, '/')
|
|
124
|
+
.replace(/[^a-zA-Z0-9/_-]+/g, '-')
|
|
125
|
+
.replace(/\/+/g, '-')
|
|
126
|
+
.replace(/-+/g, '-')
|
|
127
|
+
.replace(/^-|-$/g, '')
|
|
128
|
+
.toLowerCase();
|
|
129
|
+
return sanitized.slice(0, 48) || 'scope';
|
|
130
|
+
}
|
|
131
|
+
async function traceArtifactsExist(traceDir) {
|
|
132
|
+
const tracePath = path.join(traceDir, TRACE_FILE_NAME);
|
|
133
|
+
const typesPath = path.join(traceDir, TYPES_FILE_NAME);
|
|
134
|
+
const [traceStats, typesStats] = await Promise.all([
|
|
135
|
+
stat(tracePath).catch(() => undefined),
|
|
136
|
+
stat(typesPath).catch(() => undefined),
|
|
137
|
+
]);
|
|
138
|
+
return Boolean(traceStats?.isFile() && typesStats?.isFile());
|
|
139
|
+
}
|
|
140
|
+
function formatWorkspaceRelativePath(targetPath) {
|
|
141
|
+
const relative = path.relative(rootDir, targetPath).replace(/\\/g, '/');
|
|
142
|
+
return relative || '.';
|
|
143
|
+
}
|
|
144
|
+
function isPathInWorkspace(targetPath) {
|
|
145
|
+
const relative = path.relative(rootDir, targetPath);
|
|
146
|
+
return Boolean(relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)));
|
|
147
|
+
}
|
|
148
|
+
function toPosixPath(value) {
|
|
149
|
+
return value.replace(/\\/g, '/');
|
|
150
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-check:
|
|
3
|
+
* 1) pnpm manager-cli
|
|
4
|
+
* 2) utilities -> typescript trace profiler -> profile file/folder
|
|
5
|
+
* 3) pick target path, wait for trace generation, review offenders
|
|
6
|
+
*/
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { runHelperCli } from '../helper-cli.js';
|
|
9
|
+
import { colors } from '../utils/log.js';
|
|
10
|
+
import { parseTraceReportCliArgs, printTraceReportUsage } from './cli/options.js';
|
|
11
|
+
import { promptTraceReportSettings } from './cli/prompts.js';
|
|
12
|
+
import { loadTraceReportSettings } from './config.js';
|
|
13
|
+
import { promptTraceDirectorySelection, promptTraceProfileTargetSelection, } from './picker.js';
|
|
14
|
+
import { generateTypeScriptTraceForTarget, resolveTraceProfileTargetFromPath, } from './generate.js';
|
|
15
|
+
import { TraceReportError, runTypeScriptTraceReport } from './report.js';
|
|
16
|
+
export async function runTypeScriptTraceProfiler() {
|
|
17
|
+
console.log(colors.cyan('Loading defaults from .vscode/settings.json (manager.tsTraceProfiler)'));
|
|
18
|
+
let settings = await loadTraceReportSettings();
|
|
19
|
+
while (true) {
|
|
20
|
+
let lastAction;
|
|
21
|
+
const ran = await runHelperCli({
|
|
22
|
+
title: 'TypeScript trace profiler',
|
|
23
|
+
scripts: [
|
|
24
|
+
{
|
|
25
|
+
name: 'Profile file/folder (auto trace)',
|
|
26
|
+
emoji: '⏱️',
|
|
27
|
+
description: traceSettingsSummary(settings),
|
|
28
|
+
handler: async () => {
|
|
29
|
+
try {
|
|
30
|
+
const target = await promptTraceProfileTargetSelection();
|
|
31
|
+
if (!target)
|
|
32
|
+
return;
|
|
33
|
+
const generated = await generateTypeScriptTraceForTarget({
|
|
34
|
+
target,
|
|
35
|
+
projectPathSetting: settings.projectPath,
|
|
36
|
+
});
|
|
37
|
+
console.log(colors.magenta(`Analyzing TypeScript trace for ${target.label}`));
|
|
38
|
+
console.log(colors.dim(`Trace output: ${generated.traceDir}`));
|
|
39
|
+
await runTypeScriptTraceReport(buildRunOptions(settings, generated.traceDir));
|
|
40
|
+
lastAction = 'run';
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
logTraceError(error);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Analyze existing trace',
|
|
49
|
+
emoji: '🧾',
|
|
50
|
+
description: 'Pick an existing trace.json/types.json folder and report offenders',
|
|
51
|
+
handler: async () => {
|
|
52
|
+
try {
|
|
53
|
+
const selection = await promptTraceDirectorySelection();
|
|
54
|
+
if (!selection)
|
|
55
|
+
return;
|
|
56
|
+
console.log(colors.magenta(`Analyzing TypeScript trace from ${selection.label}`));
|
|
57
|
+
await runTypeScriptTraceReport(buildRunOptions(settings, selection.traceDir));
|
|
58
|
+
lastAction = 'run';
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
logTraceError(error);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'Adjust settings',
|
|
67
|
+
emoji: '⚙️',
|
|
68
|
+
description: traceSettingsSummary(settings),
|
|
69
|
+
handler: async () => {
|
|
70
|
+
settings = await promptTraceReportSettings(settings);
|
|
71
|
+
lastAction = 'configure';
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
argv: [],
|
|
76
|
+
});
|
|
77
|
+
if (!ran)
|
|
78
|
+
return;
|
|
79
|
+
if (lastAction === 'run')
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function runTypeScriptTraceReportCli(argv) {
|
|
84
|
+
const defaults = await loadTraceReportSettings();
|
|
85
|
+
const parsed = parseTraceReportCliArgs(argv, defaults);
|
|
86
|
+
if (parsed.help) {
|
|
87
|
+
printTraceReportUsage();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!parsed.command)
|
|
91
|
+
return;
|
|
92
|
+
if (parsed.command.mode === 'existing') {
|
|
93
|
+
await runTypeScriptTraceReport(parsed.command.report);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const target = await resolveTraceProfileTargetFromPath(parsed.command.targetPath);
|
|
97
|
+
const generated = await generateTypeScriptTraceForTarget({
|
|
98
|
+
target,
|
|
99
|
+
projectPathSetting: parsed.command.projectPath,
|
|
100
|
+
});
|
|
101
|
+
await runTypeScriptTraceReport({
|
|
102
|
+
traceDir: generated.traceDir,
|
|
103
|
+
...parsed.command.report,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function buildRunOptions(settings, traceDir) {
|
|
107
|
+
return {
|
|
108
|
+
traceDir,
|
|
109
|
+
top: settings.top,
|
|
110
|
+
minMs: settings.minMs,
|
|
111
|
+
json: settings.includeJson || Boolean(settings.outPath),
|
|
112
|
+
filterRegex: settings.filterRegex || undefined,
|
|
113
|
+
eventNames: settings.eventNames,
|
|
114
|
+
baseDir: path.resolve(settings.baseDir || process.cwd()),
|
|
115
|
+
outPath: settings.outPath || undefined,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function traceSettingsSummary(settings) {
|
|
119
|
+
const events = settings.eventNames.length
|
|
120
|
+
? settings.eventNames.join(',')
|
|
121
|
+
: 'all';
|
|
122
|
+
const filter = settings.filterRegex ? settings.filterRegex : 'none';
|
|
123
|
+
const jsonLabel = settings.includeJson || settings.outPath ? 'on' : 'off';
|
|
124
|
+
const project = settings.projectPath || 'auto';
|
|
125
|
+
return `top=${settings.top} minMs=${settings.minMs} events=${events} filter=${filter} json=${jsonLabel} project=${project}`;
|
|
126
|
+
}
|
|
127
|
+
function logTraceError(error) {
|
|
128
|
+
if (error instanceof TraceReportError) {
|
|
129
|
+
console.log(colors.red(error.message));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (error instanceof Error) {
|
|
133
|
+
console.log(colors.red(error.message));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
console.log(colors.red(String(error)));
|
|
137
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { tmpdir } from 'node:os';
|
|
2
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describeDirectorySelection, promptDirectorySelection, } from '../directory-picker.js';
|
|
5
|
+
import { IGNORED_DIRECTORIES, SOURCE_EXTENSIONS, } from '../format-checker/scan/constants.js';
|
|
6
|
+
import { rootDir } from '../helper-cli/env.js';
|
|
7
|
+
import { promptForScript } from '../helper-cli/prompts.js';
|
|
8
|
+
import { normalizeScripts } from '../helper-cli/scripts.js';
|
|
9
|
+
import { runHelperCli } from '../helper-cli.js';
|
|
10
|
+
import { askLine } from '../prompts.js';
|
|
11
|
+
import { colors } from '../utils/log.js';
|
|
12
|
+
const TRACE_FILE_NAME = 'trace.json';
|
|
13
|
+
const TYPES_FILE_NAME = 'types.json';
|
|
14
|
+
const TRACE_SCAN_MAX_DEPTH = 4;
|
|
15
|
+
const TRACE_SCAN_MAX_RESULTS = 300;
|
|
16
|
+
const PROFILE_SCAN_MAX_DEPTH = 8;
|
|
17
|
+
const PROFILE_SCAN_MAX_RESULTS = 600;
|
|
18
|
+
export async function promptTraceProfileTargetSelection() {
|
|
19
|
+
// eslint-disable-next-line no-constant-condition
|
|
20
|
+
while (true) {
|
|
21
|
+
let selected;
|
|
22
|
+
const ran = await runHelperCli({
|
|
23
|
+
title: 'TypeScript trace profiler target',
|
|
24
|
+
scripts: [
|
|
25
|
+
{
|
|
26
|
+
name: 'Pick folder from workspace',
|
|
27
|
+
emoji: '📁',
|
|
28
|
+
description: 'Profile all TypeScript work under a selected folder',
|
|
29
|
+
handler: async () => {
|
|
30
|
+
selected = await pickProfileDirectoryFromWorkspace();
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'Pick file from workspace',
|
|
35
|
+
emoji: '📄',
|
|
36
|
+
description: 'Profile a single TypeScript file',
|
|
37
|
+
handler: async () => {
|
|
38
|
+
selected = await pickProfileFileFromWorkspace();
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Enter file/folder path',
|
|
43
|
+
emoji: '⌨️',
|
|
44
|
+
description: 'Manual path entry relative to the workspace',
|
|
45
|
+
handler: async () => {
|
|
46
|
+
selected = await pickProfilePathManually();
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
argv: [],
|
|
51
|
+
});
|
|
52
|
+
if (!ran)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (selected)
|
|
55
|
+
return selected;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function promptTraceDirectorySelection() {
|
|
59
|
+
// eslint-disable-next-line no-constant-condition
|
|
60
|
+
while (true) {
|
|
61
|
+
let selected;
|
|
62
|
+
const ran = await runHelperCli({
|
|
63
|
+
title: 'TypeScript trace input (pick a folder or file)',
|
|
64
|
+
scripts: [
|
|
65
|
+
{
|
|
66
|
+
name: 'Pick folder from workspace',
|
|
67
|
+
emoji: '📁',
|
|
68
|
+
description: 'Choose a directory and auto-detect trace.json/types.json',
|
|
69
|
+
handler: async () => {
|
|
70
|
+
selected = await pickTraceFolderFromWorkspace();
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: `Pick folder from ${tmpdir()}`,
|
|
75
|
+
emoji: '🧊',
|
|
76
|
+
description: 'Quick pick for /tmp/ts-trace-* directories',
|
|
77
|
+
handler: async () => {
|
|
78
|
+
selected = await pickTraceFolderFromTmp();
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'Pick trace.json file from workspace',
|
|
83
|
+
emoji: '📄',
|
|
84
|
+
description: 'Choose a file; its parent directory is used as traceDir',
|
|
85
|
+
handler: async () => {
|
|
86
|
+
selected = await pickTraceFileFromWorkspace();
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'Enter file or folder path',
|
|
91
|
+
emoji: '⌨️',
|
|
92
|
+
description: 'Manual path entry for any location',
|
|
93
|
+
handler: async () => {
|
|
94
|
+
selected = await pickTracePathManually();
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
argv: [],
|
|
99
|
+
});
|
|
100
|
+
if (!ran)
|
|
101
|
+
return undefined;
|
|
102
|
+
if (selected)
|
|
103
|
+
return selected;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function pickProfileDirectoryFromWorkspace() {
|
|
107
|
+
const selection = await promptDirectorySelection({
|
|
108
|
+
title: 'Select a directory to profile',
|
|
109
|
+
});
|
|
110
|
+
if (!selection)
|
|
111
|
+
return undefined;
|
|
112
|
+
return {
|
|
113
|
+
absolutePath: selection.absolutePath,
|
|
114
|
+
label: describeDirectorySelection(selection),
|
|
115
|
+
kind: 'directory',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function pickProfileFileFromWorkspace() {
|
|
119
|
+
const baseSelection = await promptDirectorySelection({
|
|
120
|
+
title: 'Select a directory to search for source files',
|
|
121
|
+
});
|
|
122
|
+
if (!baseSelection)
|
|
123
|
+
return undefined;
|
|
124
|
+
const files = await collectProfileFiles(baseSelection.absolutePath, PROFILE_SCAN_MAX_DEPTH, PROFILE_SCAN_MAX_RESULTS);
|
|
125
|
+
if (!files.length) {
|
|
126
|
+
console.log(colors.yellow(`No TypeScript/JavaScript source files found under ${baseSelection.relativePath || '.'}.`));
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const chosen = await promptPathCandidate(files, 'Pick file to profile', baseSelection.absolutePath, '📄');
|
|
130
|
+
if (!chosen)
|
|
131
|
+
return undefined;
|
|
132
|
+
return {
|
|
133
|
+
absolutePath: chosen.absolutePath,
|
|
134
|
+
label: relToRoot(chosen.absolutePath),
|
|
135
|
+
kind: 'file',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function pickProfilePathManually() {
|
|
139
|
+
const rawPath = await askLine(colors.cyan('Enter workspace-relative file/folder path to profile: '));
|
|
140
|
+
const trimmed = rawPath.trim();
|
|
141
|
+
if (!trimmed)
|
|
142
|
+
return undefined;
|
|
143
|
+
const absolutePath = path.isAbsolute(trimmed)
|
|
144
|
+
? path.resolve(trimmed)
|
|
145
|
+
: path.resolve(rootDir, trimmed);
|
|
146
|
+
let stats;
|
|
147
|
+
try {
|
|
148
|
+
stats = await stat(absolutePath);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
console.log(colors.yellow(`Path not found: ${absolutePath}`));
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
if (!stats.isFile() && !stats.isDirectory()) {
|
|
155
|
+
console.log(colors.yellow('Target must be a file or a folder.'));
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
absolutePath,
|
|
160
|
+
label: relToRoot(absolutePath),
|
|
161
|
+
kind: stats.isDirectory() ? 'directory' : 'file',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function pickTraceFolderFromWorkspace() {
|
|
165
|
+
const selection = await promptDirectorySelection({
|
|
166
|
+
title: 'Select a directory containing a TypeScript trace',
|
|
167
|
+
});
|
|
168
|
+
if (!selection)
|
|
169
|
+
return undefined;
|
|
170
|
+
return resolveTraceDirectoryFromInput(selection.absolutePath);
|
|
171
|
+
}
|
|
172
|
+
async function pickTraceFolderFromTmp() {
|
|
173
|
+
const tmp = tmpdir();
|
|
174
|
+
let entries;
|
|
175
|
+
try {
|
|
176
|
+
entries = await readdir(tmp, { withFileTypes: true });
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
console.log(colors.yellow(`Unable to read temporary directory: ${tmp}`));
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const candidates = [];
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
if (!entry.isDirectory())
|
|
185
|
+
continue;
|
|
186
|
+
if (!entry.name.startsWith('ts-trace-'))
|
|
187
|
+
continue;
|
|
188
|
+
const absolutePath = path.join(tmp, entry.name);
|
|
189
|
+
const hasArtifacts = await directoryHasTraceArtifacts(absolutePath);
|
|
190
|
+
if (!hasArtifacts)
|
|
191
|
+
continue;
|
|
192
|
+
candidates.push({
|
|
193
|
+
absolutePath,
|
|
194
|
+
relativePath: absolutePath.replace(/\\/g, '/'),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (!candidates.length) {
|
|
198
|
+
console.log(colors.yellow(`No ts-trace-* folders with trace artifacts found in ${tmp}.`));
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
const chosen = await promptPathCandidate(candidates, `Pick a trace folder from ${tmp}`, tmp, '📦');
|
|
202
|
+
if (!chosen)
|
|
203
|
+
return undefined;
|
|
204
|
+
return {
|
|
205
|
+
traceDir: chosen.absolutePath,
|
|
206
|
+
label: chosen.relativePath,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async function pickTraceFileFromWorkspace() {
|
|
210
|
+
const baseSelection = await promptDirectorySelection({
|
|
211
|
+
title: 'Select a directory to search for trace.json files',
|
|
212
|
+
});
|
|
213
|
+
if (!baseSelection)
|
|
214
|
+
return undefined;
|
|
215
|
+
const files = await collectNamedFiles(baseSelection.absolutePath, TRACE_FILE_NAME, TRACE_SCAN_MAX_DEPTH, TRACE_SCAN_MAX_RESULTS);
|
|
216
|
+
if (!files.length) {
|
|
217
|
+
console.log(colors.yellow('No trace.json files found in that selection.'));
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
const chosen = await promptPathCandidate(files, 'Pick trace.json file', baseSelection.absolutePath, '📦');
|
|
221
|
+
if (!chosen)
|
|
222
|
+
return undefined;
|
|
223
|
+
return resolveTraceDirectoryFromInput(chosen.absolutePath);
|
|
224
|
+
}
|
|
225
|
+
async function pickTracePathManually() {
|
|
226
|
+
const rawPath = await askLine(colors.cyan('Enter path to trace directory or trace.json/types.json file: '));
|
|
227
|
+
const trimmed = rawPath.trim();
|
|
228
|
+
if (!trimmed)
|
|
229
|
+
return undefined;
|
|
230
|
+
const absolutePath = path.isAbsolute(trimmed)
|
|
231
|
+
? trimmed
|
|
232
|
+
: path.resolve(rootDir, trimmed);
|
|
233
|
+
return resolveTraceDirectoryFromInput(absolutePath);
|
|
234
|
+
}
|
|
235
|
+
async function resolveTraceDirectoryFromInput(inputPath) {
|
|
236
|
+
let targetPath = inputPath;
|
|
237
|
+
let stats;
|
|
238
|
+
try {
|
|
239
|
+
stats = await stat(targetPath);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
console.log(colors.yellow(`Path not found: ${targetPath}`));
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
if (stats.isFile()) {
|
|
246
|
+
const fileName = path.basename(targetPath).toLowerCase();
|
|
247
|
+
if (fileName !== TRACE_FILE_NAME && fileName !== TYPES_FILE_NAME) {
|
|
248
|
+
console.log(colors.yellow('Selected file must be trace.json or types.json. Pick one of those files or a folder.'));
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
targetPath = path.dirname(targetPath);
|
|
252
|
+
}
|
|
253
|
+
if (!(await isDirectory(targetPath))) {
|
|
254
|
+
console.log(colors.yellow(`Not a directory: ${targetPath}`));
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
if (await directoryHasTraceArtifacts(targetPath)) {
|
|
258
|
+
return {
|
|
259
|
+
traceDir: targetPath,
|
|
260
|
+
label: relToRoot(targetPath),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const nestedCandidates = await collectTraceDirectories(targetPath, TRACE_SCAN_MAX_DEPTH, TRACE_SCAN_MAX_RESULTS);
|
|
264
|
+
if (!nestedCandidates.length) {
|
|
265
|
+
console.log(colors.yellow(`No folders containing ${TRACE_FILE_NAME} and ${TYPES_FILE_NAME} were found.`));
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
const chosen = await promptPathCandidate(nestedCandidates, 'Select a trace directory', targetPath, '📦');
|
|
269
|
+
if (!chosen)
|
|
270
|
+
return undefined;
|
|
271
|
+
return {
|
|
272
|
+
traceDir: chosen.absolutePath,
|
|
273
|
+
label: chosen.relativePath,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
async function promptPathCandidate(candidates, title, basePath, emoji) {
|
|
277
|
+
const entries = normalizeScripts(candidates.map((candidate) => ({
|
|
278
|
+
name: candidate.relativePath,
|
|
279
|
+
emoji,
|
|
280
|
+
description: candidate.absolutePath,
|
|
281
|
+
script: candidate.absolutePath,
|
|
282
|
+
})));
|
|
283
|
+
const chosen = await promptForScript(entries, title);
|
|
284
|
+
if (!chosen || !chosen.script)
|
|
285
|
+
return undefined;
|
|
286
|
+
const absolutePath = chosen.script;
|
|
287
|
+
return {
|
|
288
|
+
absolutePath,
|
|
289
|
+
relativePath: path.relative(basePath, absolutePath).replace(/\\/g, '/') || '.',
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
async function collectNamedFiles(rootPath, filename, maxDepth, maxResults) {
|
|
293
|
+
const output = [];
|
|
294
|
+
await walk(rootPath, rootPath, 0, async (absolutePath, relativePath) => {
|
|
295
|
+
if (path.basename(absolutePath).toLowerCase() !== filename)
|
|
296
|
+
return;
|
|
297
|
+
if (output.length >= maxResults)
|
|
298
|
+
return;
|
|
299
|
+
output.push({ absolutePath, relativePath });
|
|
300
|
+
}, maxDepth);
|
|
301
|
+
return output;
|
|
302
|
+
}
|
|
303
|
+
async function collectTraceDirectories(rootPath, maxDepth, maxResults) {
|
|
304
|
+
const output = [];
|
|
305
|
+
await walk(rootPath, rootPath, 0, async (absolutePath, relativePath) => {
|
|
306
|
+
if (output.length >= maxResults)
|
|
307
|
+
return;
|
|
308
|
+
if (!(await isDirectory(absolutePath)))
|
|
309
|
+
return;
|
|
310
|
+
const hasArtifacts = await directoryHasTraceArtifacts(absolutePath);
|
|
311
|
+
if (!hasArtifacts)
|
|
312
|
+
return;
|
|
313
|
+
output.push({ absolutePath, relativePath });
|
|
314
|
+
}, maxDepth);
|
|
315
|
+
return output;
|
|
316
|
+
}
|
|
317
|
+
async function collectProfileFiles(rootPath, maxDepth, maxResults) {
|
|
318
|
+
const output = [];
|
|
319
|
+
await walk(rootPath, rootPath, 0, async (absolutePath, relativePath) => {
|
|
320
|
+
if (output.length >= maxResults)
|
|
321
|
+
return;
|
|
322
|
+
const extension = path.extname(absolutePath).toLowerCase();
|
|
323
|
+
if (!SOURCE_EXTENSIONS.has(extension))
|
|
324
|
+
return;
|
|
325
|
+
output.push({ absolutePath, relativePath });
|
|
326
|
+
}, maxDepth);
|
|
327
|
+
return output;
|
|
328
|
+
}
|
|
329
|
+
async function walk(currentPath, basePath, depth, onEntry, maxDepth) {
|
|
330
|
+
if (depth > maxDepth)
|
|
331
|
+
return;
|
|
332
|
+
let entries;
|
|
333
|
+
try {
|
|
334
|
+
entries = await readdir(currentPath, { withFileTypes: true });
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
for (const entry of entries) {
|
|
340
|
+
if (entry.isSymbolicLink())
|
|
341
|
+
continue;
|
|
342
|
+
if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name))
|
|
343
|
+
continue;
|
|
344
|
+
const absolutePath = path.join(currentPath, entry.name);
|
|
345
|
+
const relativePath = path.relative(basePath, absolutePath).replace(/\\/g, '/') || '.';
|
|
346
|
+
await onEntry(absolutePath, relativePath);
|
|
347
|
+
if (entry.isDirectory()) {
|
|
348
|
+
await walk(absolutePath, basePath, depth + 1, onEntry, maxDepth);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function directoryHasTraceArtifacts(directory) {
|
|
353
|
+
const tracePath = path.join(directory, TRACE_FILE_NAME);
|
|
354
|
+
const typesPath = path.join(directory, TYPES_FILE_NAME);
|
|
355
|
+
const [traceStats, typesStats] = await Promise.all([
|
|
356
|
+
stat(tracePath).catch(() => undefined),
|
|
357
|
+
stat(typesPath).catch(() => undefined),
|
|
358
|
+
]);
|
|
359
|
+
return Boolean(traceStats?.isFile() && typesStats?.isFile());
|
|
360
|
+
}
|
|
361
|
+
async function isDirectory(targetPath) {
|
|
362
|
+
try {
|
|
363
|
+
const stats = await stat(targetPath);
|
|
364
|
+
return stats.isDirectory();
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function relToRoot(targetPath) {
|
|
371
|
+
const relative = path.relative(rootDir, targetPath).replace(/\\/g, '/');
|
|
372
|
+
return relative || '.';
|
|
373
|
+
}
|