@emeryld/manager 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/dist/directory-picker.js +158 -0
- package/dist/format-checker/cli/prompts.js +9 -7
- package/dist/format-checker/index.js +56 -6
- package/dist/robot/cli/prompts.js +10 -4
- package/dist/robot/cli/settings.js +16 -0
- package/dist/robot/config.js +11 -0
- package/dist/robot/coordinator.js +81 -24
- package/dist/robot/extractors/classes.js +14 -13
- package/dist/robot/extractors/components.js +17 -10
- package/dist/robot/extractors/constants.js +9 -6
- package/dist/robot/extractors/functions.js +11 -8
- package/dist/robot/extractors/shared.js +6 -1
- package/dist/robot/extractors/types.js +5 -8
- package/dist/robot/serializer.js +97 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,8 +65,73 @@ You can also decide where the report lands: the manager will ask whether to stre
|
|
|
65
65
|
|
|
66
66
|
The `robot metadata` action now starts with the same interactive settings screen as the format checker: pick which kinds of symbols to include, decide whether to limit the scan to exported declarations, and adjust the column width. Use ↑/↓ to move between rows, type new values (comma-separated lists for the kinds), and press Enter once the validation message disappears.
|
|
67
67
|
|
|
68
|
+
The column width now controls how much context is emitted for each declaration. Symbols that start past the configured column keep their name/export status but drop heavyweight fields (IO signatures, docstrings, JSX locations, etc.), so you still see everything in the workspace but only the left-most definitions contribute rich metadata to your LLM prompt.
|
|
69
|
+
|
|
68
70
|
Before the extraction runs you can also choose how to consume the results. The default `console` stream prints the JSON into your terminal, while `editor` writes the report to a temporary file (non-saved) and tries to open it in your editor via your configured `$EDITOR`, `code`, or the OS `open/xdg-open` helpers.
|
|
69
71
|
|
|
72
|
+
To keep the payload more token-friendly, the interactive settings screen now exposes `Condense output for compact JSON` and `Docstring length limit`. Enabling the condensed output replaces the verbose object format with a small `fields` + `rows` array layout, and the docstring limit trims comments to the first N characters (set the value to `0` for unlimited). Set your preferred defaults in `.vscode/settings.json` under `manager.robot.condenseOutput` and `manager.robot.maxDocStringLength`.
|
|
73
|
+
|
|
74
|
+
### Running with arguments
|
|
75
|
+
Run `pnpm manager-cli`, pick the workspace (or “All packages”), then select the `robot metadata` action. The helper walks you through:
|
|
76
|
+
|
|
77
|
+
1. Set the symbol kinds to capture (comma-separated list or `all`).
|
|
78
|
+
2. Toggle `Only exported symbols`, adjust `Maximum columns`, `Condense output for compact JSON`, and `Docstring length limit`.
|
|
79
|
+
3. Confirm the summary to trigger the run and choose whether to stream results to the console or editor.
|
|
80
|
+
|
|
81
|
+
The same knobs live in `.vscode/settings.json` under `manager.robot`, for example:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"manager.robot": {
|
|
86
|
+
"includeKinds": ["function", "type", "const"],
|
|
87
|
+
"exportedOnly": true,
|
|
88
|
+
"maxColumns": 120,
|
|
89
|
+
"condenseOutput": true,
|
|
90
|
+
"maxDocStringLength": 80
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Example output
|
|
96
|
+
`robot metadata` prints a summary followed by the parsed payload. With the default formatting you get readable JSON:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
Robot extraction complete (functions=4 components=1 types=2 consts=0 classes=1)
|
|
100
|
+
Estimated tokens: 72
|
|
101
|
+
Detailed results:
|
|
102
|
+
{
|
|
103
|
+
"functions": [
|
|
104
|
+
{
|
|
105
|
+
"kind": "function",
|
|
106
|
+
"name": "buildManagerConfig",
|
|
107
|
+
"location": { "file": "src/menu.ts", "line": 45, "column": 3 },
|
|
108
|
+
"docString": "Builds the menu configuration for the helper CLI.",
|
|
109
|
+
"exported": true,
|
|
110
|
+
"inputs": ["packages: LoadedPackage[]"],
|
|
111
|
+
"output": "HelperScriptEntry[]"
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
...
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Enabling `Condense output for compact JSON` yields a compact payload that reduces tokens even when the detailed rows are long:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
Robot extraction complete (functions=4 components=1 types=2 consts=0 classes=1)
|
|
122
|
+
Estimated tokens: 48
|
|
123
|
+
Detailed results (condensed):
|
|
124
|
+
{
|
|
125
|
+
"version": 1,
|
|
126
|
+
"summary": { "functions": 4, "components": 1, "types": 2, "consts": 0, "classes": 1 },
|
|
127
|
+
"fields": ["kind","file","line","column","name","signature","docString","exported"],
|
|
128
|
+
"rows": [
|
|
129
|
+
["function","src/menu.ts",45,3,"buildManagerConfig","(packages: LoadedPackage[]) => HelperScriptEntry[]","Builds the menu configuration...",true],
|
|
130
|
+
["component","src/ui/banner.ts",12,7,"Banner","(props: BannerProps) => JSX.Element","Lightweight banner used in the hero layout.",true]
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
70
135
|
## Non-interactive release (Codex/CI)
|
|
71
136
|
- **Syntax**: `pnpm manager-cli <pkg|all> --non-interactive [publish flags]`
|
|
72
137
|
- **Requirements**: provide the selection (`<pkg>` or `all`) and one of `--bump <type>`, `--sync <version>`, or `--noop` (skip the version change but still tag/publish). Use `--non-interactive`, `--ci`, `--yes`, or `-y` interchangeably to answer every prompt in the affirmative.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { IGNORED_DIRECTORIES } from './format-checker/scan/constants.js';
|
|
4
|
+
import { normalizeScripts } from './helper-cli/scripts.js';
|
|
5
|
+
import { promptForScript } from './helper-cli/prompts.js';
|
|
6
|
+
import { rootDir } from './helper-cli/env.js';
|
|
7
|
+
import { loadPackages } from './packages.js';
|
|
8
|
+
const ROOT_ENTRY = {
|
|
9
|
+
label: 'Workspace root',
|
|
10
|
+
relativePath: '.',
|
|
11
|
+
description: 'Root directory',
|
|
12
|
+
color: 'brightBlue',
|
|
13
|
+
};
|
|
14
|
+
export async function promptDirectorySelection(options = {}) {
|
|
15
|
+
const candidates = await buildDirectoryCandidates(options.includeWorkspaceRoot ?? true);
|
|
16
|
+
const entries = normalizeScripts(candidates.map((candidate) => ({
|
|
17
|
+
name: candidate.label,
|
|
18
|
+
emoji: '📁',
|
|
19
|
+
color: candidate.color,
|
|
20
|
+
description: candidate.description ?? candidate.relativePath,
|
|
21
|
+
script: candidate.relativePath,
|
|
22
|
+
})));
|
|
23
|
+
const selection = await promptForScript(entries, options.title ?? 'Select a directory');
|
|
24
|
+
if (!selection)
|
|
25
|
+
return undefined;
|
|
26
|
+
const relativePath = selection.script ?? '.';
|
|
27
|
+
const absolutePath = path.resolve(rootDir, relativePath);
|
|
28
|
+
return { label: selection.displayName, relativePath, absolutePath };
|
|
29
|
+
}
|
|
30
|
+
export function describeDirectorySelection(selection) {
|
|
31
|
+
const normalized = selection.relativePath
|
|
32
|
+
.replace(/\\/g, '/')
|
|
33
|
+
.replace(/^\.\/+/, '');
|
|
34
|
+
if (!normalized || normalized === '.') {
|
|
35
|
+
return selection.label;
|
|
36
|
+
}
|
|
37
|
+
return `${selection.label} (${normalized})`;
|
|
38
|
+
}
|
|
39
|
+
async function buildDirectoryCandidates(includeRoot) {
|
|
40
|
+
const directories = await collectDirectories();
|
|
41
|
+
const packages = await loadPackageMetadata();
|
|
42
|
+
const packageMap = new Map(packages.map((pkg) => [pkg.relativeDir, pkg]));
|
|
43
|
+
const candidates = [];
|
|
44
|
+
const filtered = directories.filter((entry) => includeRoot || entry.relativePath !== '.');
|
|
45
|
+
for (const entry of filtered) {
|
|
46
|
+
const pkg = packageMap.get(entry.relativePath);
|
|
47
|
+
if (pkg) {
|
|
48
|
+
candidates.push({
|
|
49
|
+
label: pkg.name ?? pkg.dirName,
|
|
50
|
+
relativePath: entry.relativePath,
|
|
51
|
+
description: pkg.relativeDir,
|
|
52
|
+
color: pkg.color,
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (entry.relativePath === '.') {
|
|
57
|
+
candidates.push({
|
|
58
|
+
label: ROOT_ENTRY.label,
|
|
59
|
+
relativePath: ROOT_ENTRY.relativePath,
|
|
60
|
+
description: ROOT_ENTRY.description,
|
|
61
|
+
color: ROOT_ENTRY.color,
|
|
62
|
+
});
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
candidates.push({
|
|
66
|
+
label: path.basename(entry.relativePath) || ROOT_ENTRY.label,
|
|
67
|
+
relativePath: entry.relativePath,
|
|
68
|
+
description: entry.relativePath,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (candidates.length === 0 && includeRoot) {
|
|
72
|
+
candidates.push(ROOT_ENTRY);
|
|
73
|
+
}
|
|
74
|
+
return candidates;
|
|
75
|
+
}
|
|
76
|
+
async function collectDirectories() {
|
|
77
|
+
const patterns = await collectIgnorePatterns();
|
|
78
|
+
const directories = [];
|
|
79
|
+
async function walk(current) {
|
|
80
|
+
const relativeRaw = path.relative(rootDir, current);
|
|
81
|
+
const relativePath = normalizeRelativePath(relativeRaw);
|
|
82
|
+
if (current !== rootDir && shouldIgnore(relativePath, patterns)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
directories.push({ absolutePath: current, relativePath });
|
|
86
|
+
let entries;
|
|
87
|
+
try {
|
|
88
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
95
|
+
continue;
|
|
96
|
+
await walk(path.join(current, entry.name));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
await walk(rootDir);
|
|
100
|
+
directories.sort((a, b) => {
|
|
101
|
+
if (a.relativePath === '.')
|
|
102
|
+
return -1;
|
|
103
|
+
if (b.relativePath === '.')
|
|
104
|
+
return 1;
|
|
105
|
+
return a.relativePath.localeCompare(b.relativePath);
|
|
106
|
+
});
|
|
107
|
+
return directories;
|
|
108
|
+
}
|
|
109
|
+
async function collectIgnorePatterns() {
|
|
110
|
+
const patterns = new Set();
|
|
111
|
+
for (const entry of IGNORED_DIRECTORIES) {
|
|
112
|
+
patterns.add(normalizeRelativePath(entry));
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const contents = await readFile(path.join(rootDir, '.gitignore'), 'utf-8');
|
|
116
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
117
|
+
const trimmed = rawLine.trim();
|
|
118
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('!'))
|
|
119
|
+
continue;
|
|
120
|
+
patterns.add(normalizeRelativePath(trimmed));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// ignore failures (missing file)
|
|
125
|
+
}
|
|
126
|
+
return Array.from(patterns).filter(Boolean);
|
|
127
|
+
}
|
|
128
|
+
function normalizeRelativePath(value) {
|
|
129
|
+
const normalized = value.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
130
|
+
const trimmed = normalized.replace(/\/+$/, '');
|
|
131
|
+
return trimmed || '.';
|
|
132
|
+
}
|
|
133
|
+
function shouldIgnore(relativePath, patterns) {
|
|
134
|
+
if (relativePath === '.')
|
|
135
|
+
return false;
|
|
136
|
+
const segments = relativePath.split('/');
|
|
137
|
+
for (const pattern of patterns) {
|
|
138
|
+
if (!pattern)
|
|
139
|
+
continue;
|
|
140
|
+
if (relativePath === pattern)
|
|
141
|
+
return true;
|
|
142
|
+
if (relativePath.startsWith(`${pattern}/`))
|
|
143
|
+
return true;
|
|
144
|
+
const patternSegments = pattern.split('/');
|
|
145
|
+
if (patternSegments.length === 1 && segments.includes(pattern)) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
async function loadPackageMetadata() {
|
|
152
|
+
try {
|
|
153
|
+
return await loadPackages();
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { colors } from '../../utils/log.js';
|
|
2
2
|
import { stdin as input } from 'node:process';
|
|
3
|
-
import { askLine } from '../../prompts.js';
|
|
3
|
+
import { askLine, promptSingleKey } from '../../prompts.js';
|
|
4
4
|
import { formatValue, parseInteractiveValue, SETTING_DESCRIPTORS, validateLimits, } from './settings.js';
|
|
5
5
|
import { promptInteractiveSettings, } from '../../cli/interactive-settings.js';
|
|
6
6
|
const READY_PROMPT = colors.dim('Limits retained. Use the navigation above to adjust and confirm again.');
|
|
@@ -20,14 +20,16 @@ export async function promptLimits(defaults) {
|
|
|
20
20
|
}
|
|
21
21
|
async function confirmExecution() {
|
|
22
22
|
const question = colors.cyan('Run the format checker with these limits? (Y/n): ');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
return promptSingleKey(question, (key, raw) => {
|
|
24
|
+
if (!key)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (key === 'y' || key === 'yes')
|
|
26
27
|
return true;
|
|
27
|
-
if (
|
|
28
|
+
if (key === 'n' || key === 'no')
|
|
28
29
|
return false;
|
|
29
|
-
console.log(colors.yellow('Answer "yes" or "no"
|
|
30
|
-
|
|
30
|
+
console.log(colors.yellow('Answer "yes" or "no".'));
|
|
31
|
+
return undefined;
|
|
32
|
+
});
|
|
31
33
|
}
|
|
32
34
|
async function promptLimitsSequential(defaults) {
|
|
33
35
|
console.log(colors.dim('Enter a number to override a limit or press Enter to keep the default.'));
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { colors } from '../utils/log.js';
|
|
2
2
|
import { captureConsoleOutput, exportReportLines, promptExportMode, } from '../utils/export.js';
|
|
3
|
+
import { promptDirectorySelection, describeDirectorySelection } from '../directory-picker.js';
|
|
4
|
+
import { runHelperCli } from '../helper-cli.js';
|
|
3
5
|
import { rootDir } from '../helper-cli/env.js';
|
|
4
6
|
import { loadFormatLimits, } from './config.js';
|
|
5
7
|
import { collectSourceFiles, analyzeFiles } from './scan/index.js';
|
|
@@ -9,9 +11,54 @@ import { parseScanCliArgs, printScanUsage } from './cli/options.js';
|
|
|
9
11
|
export async function runFormatChecker() {
|
|
10
12
|
console.log(colors.cyan('Gathering defaults from .vscode/settings.json (manager.formatChecker)'));
|
|
11
13
|
const defaults = await loadFormatLimits();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
let limits = defaults;
|
|
15
|
+
let exportMode = 'console';
|
|
16
|
+
while (true) {
|
|
17
|
+
let lastAction;
|
|
18
|
+
const scripts = [
|
|
19
|
+
{
|
|
20
|
+
name: 'Run format checker',
|
|
21
|
+
emoji: '🧮',
|
|
22
|
+
description: `${formatLimitsSummary(limits)} · export=${exportMode}`,
|
|
23
|
+
handler: async () => {
|
|
24
|
+
const selection = await promptDirectorySelection({
|
|
25
|
+
title: 'Select directory for format scan',
|
|
26
|
+
});
|
|
27
|
+
if (!selection)
|
|
28
|
+
return;
|
|
29
|
+
lastAction = 'run';
|
|
30
|
+
await executeFormatCheck(limits, exportMode, selection.absolutePath, describeDirectorySelection(selection));
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'Adjust limits',
|
|
35
|
+
emoji: '⚙️',
|
|
36
|
+
description: formatLimitsSummary(limits),
|
|
37
|
+
handler: async () => {
|
|
38
|
+
limits = await promptLimits(limits);
|
|
39
|
+
lastAction = 'configure';
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Change export mode',
|
|
44
|
+
emoji: '📤',
|
|
45
|
+
description: `Current: ${exportMode}`,
|
|
46
|
+
handler: async () => {
|
|
47
|
+
exportMode = await promptExportMode();
|
|
48
|
+
lastAction = 'export';
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
const ran = await runHelperCli({
|
|
53
|
+
title: 'Format checker helper',
|
|
54
|
+
scripts,
|
|
55
|
+
argv: [],
|
|
56
|
+
});
|
|
57
|
+
if (!ran)
|
|
58
|
+
return;
|
|
59
|
+
if (lastAction === 'run')
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
15
62
|
}
|
|
16
63
|
export async function runFormatCheckerScanCli(argv) {
|
|
17
64
|
const { overrides, help, exportMode } = parseScanCliArgs(argv);
|
|
@@ -24,9 +71,9 @@ export async function runFormatCheckerScanCli(argv) {
|
|
|
24
71
|
console.log(colors.cyan('Running format checker scan (machine-friendly)'));
|
|
25
72
|
await executeFormatCheck(limits, exportMode);
|
|
26
73
|
}
|
|
27
|
-
async function executeFormatCheck(limits, exportMode) {
|
|
28
|
-
console.log(colors.magenta(
|
|
29
|
-
const files = await collectSourceFiles(
|
|
74
|
+
async function executeFormatCheck(limits, exportMode, scanRoot = rootDir, scopeLabel = 'workspace') {
|
|
75
|
+
console.log(colors.magenta(`Scanning ${scopeLabel} for source files...`));
|
|
76
|
+
const files = await collectSourceFiles(scanRoot);
|
|
30
77
|
if (files.length === 0) {
|
|
31
78
|
console.log(colors.yellow('No source files were found to analyze.'));
|
|
32
79
|
return;
|
|
@@ -46,3 +93,6 @@ async function handleViolations(violations, reportingMode, exportMode) {
|
|
|
46
93
|
await exportReportLines('format-checker', 'txt', lines);
|
|
47
94
|
return result;
|
|
48
95
|
}
|
|
96
|
+
function formatLimitsSummary(limits) {
|
|
97
|
+
return `limits: fn≤${limits.maxFunctionLength} · indent≤${limits.maxIndentationDepth} · reporting=${limits.reportingMode} · exportOnly=${limits.exportOnly}`;
|
|
98
|
+
}
|
|
@@ -35,6 +35,8 @@ async function promptRobotSettingsSequential(defaults) {
|
|
|
35
35
|
includeKinds: await promptKinds(defaults.includeKinds),
|
|
36
36
|
exportedOnly: await promptBoolean('Only consider exported symbols', defaults.exportedOnly),
|
|
37
37
|
maxColumns: await promptNumber('Maximum columns', defaults.maxColumns),
|
|
38
|
+
condenseOutput: await promptBoolean('Condense output for compact JSON', defaults.condenseOutput),
|
|
39
|
+
maxDocStringLength: await promptNumber('Docstring length limit (0 = unlimited)', defaults.maxDocStringLength, { allowZero: true }),
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
42
|
async function promptKinds(fallback) {
|
|
@@ -68,17 +70,21 @@ async function promptBoolean(label, fallback) {
|
|
|
68
70
|
console.log(colors.yellow('Please answer "yes" or "no", or leave blank to keep the default.'));
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
|
-
async function promptNumber(label, fallback) {
|
|
73
|
+
async function promptNumber(label, fallback, options) {
|
|
72
74
|
const question = colors.cyan(`${label} [default ${fallback}]: `);
|
|
75
|
+
const min = options?.allowZero ? 0 : 1;
|
|
76
|
+
const description = options?.allowZero ? 'non-negative integer' : 'positive integer';
|
|
73
77
|
while (true) {
|
|
74
78
|
const answer = await askLine(question);
|
|
75
79
|
if (!answer)
|
|
76
80
|
return fallback;
|
|
77
81
|
const parsed = Number(answer);
|
|
78
|
-
if (!Number.isNaN(parsed)
|
|
79
|
-
|
|
82
|
+
if (!Number.isNaN(parsed)) {
|
|
83
|
+
const floored = Math.floor(parsed);
|
|
84
|
+
if (floored >= min)
|
|
85
|
+
return floored;
|
|
80
86
|
}
|
|
81
|
-
console.log(colors.yellow(
|
|
87
|
+
console.log(colors.yellow(`Provide a ${description} or leave blank to keep the default.`));
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
async function promptRobotSettingsInteractive(defaults) {
|
|
@@ -18,6 +18,18 @@ export const SETTING_DESCRIPTORS = [
|
|
|
18
18
|
unit: 'columns',
|
|
19
19
|
type: 'number',
|
|
20
20
|
},
|
|
21
|
+
{
|
|
22
|
+
key: 'condenseOutput',
|
|
23
|
+
label: 'Condense output for compact JSON',
|
|
24
|
+
type: 'boolean',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: 'maxDocStringLength',
|
|
28
|
+
label: 'Docstring length limit (0=unlimited)',
|
|
29
|
+
unit: 'characters',
|
|
30
|
+
type: 'number',
|
|
31
|
+
allowZero: true,
|
|
32
|
+
},
|
|
21
33
|
];
|
|
22
34
|
export function formatValue(value, descriptor) {
|
|
23
35
|
if (descriptor.type === 'boolean') {
|
|
@@ -63,6 +75,10 @@ export function validateRobotSettings(settings) {
|
|
|
63
75
|
if (!Number.isFinite(settings.maxColumns) || settings.maxColumns <= 0) {
|
|
64
76
|
return 'Maximum columns must be a positive number.';
|
|
65
77
|
}
|
|
78
|
+
if (!Number.isFinite(settings.maxDocStringLength) ||
|
|
79
|
+
settings.maxDocStringLength < 0) {
|
|
80
|
+
return 'Docstring length limit must be zero or greater.';
|
|
81
|
+
}
|
|
66
82
|
return undefined;
|
|
67
83
|
}
|
|
68
84
|
function parsePositiveInteger(inputValue, options) {
|
package/dist/robot/config.js
CHANGED
|
@@ -9,6 +9,8 @@ export const DEFAULT_ROBOT_SETTINGS = {
|
|
|
9
9
|
includeKinds: [...ROBOT_KINDS],
|
|
10
10
|
exportedOnly: true,
|
|
11
11
|
maxColumns: 160,
|
|
12
|
+
condenseOutput: true,
|
|
13
|
+
maxDocStringLength: 0,
|
|
12
14
|
};
|
|
13
15
|
function coerceNumber(value, fallback) {
|
|
14
16
|
const num = Number(value);
|
|
@@ -28,6 +30,13 @@ function coerceBoolean(value, fallback) {
|
|
|
28
30
|
}
|
|
29
31
|
return fallback;
|
|
30
32
|
}
|
|
33
|
+
function coerceNonNegativeNumber(value, fallback) {
|
|
34
|
+
const num = Number(value);
|
|
35
|
+
if (Number.isNaN(num) || num < 0) {
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
38
|
+
return Math.floor(num);
|
|
39
|
+
}
|
|
31
40
|
function coerceKinds(value) {
|
|
32
41
|
const knownKinds = new Set(ROBOT_KINDS);
|
|
33
42
|
if (typeof value === 'string') {
|
|
@@ -75,6 +84,8 @@ export async function loadRobotSettings() {
|
|
|
75
84
|
includeKinds: coerceKinds(record.includeKinds ?? record.kinds),
|
|
76
85
|
exportedOnly: coerceBoolean(record.exportedOnly ?? DEFAULT_ROBOT_SETTINGS.exportedOnly, DEFAULT_ROBOT_SETTINGS.exportedOnly),
|
|
77
86
|
maxColumns: coerceNumber(record.maxColumns ?? DEFAULT_ROBOT_SETTINGS.maxColumns, DEFAULT_ROBOT_SETTINGS.maxColumns),
|
|
87
|
+
condenseOutput: coerceBoolean(record.condenseOutput ?? DEFAULT_ROBOT_SETTINGS.condenseOutput, DEFAULT_ROBOT_SETTINGS.condenseOutput),
|
|
88
|
+
maxDocStringLength: coerceNonNegativeNumber(record.maxDocStringLength ?? DEFAULT_ROBOT_SETTINGS.maxDocStringLength, DEFAULT_ROBOT_SETTINGS.maxDocStringLength),
|
|
78
89
|
};
|
|
79
90
|
}
|
|
80
91
|
catch {
|
|
@@ -4,7 +4,8 @@ import { colors } from '../utils/log.js';
|
|
|
4
4
|
import { exportReportLines, promptExportMode, } from '../utils/export.js';
|
|
5
5
|
import { collectSourceFiles } from '../format-checker/scan/collect.js';
|
|
6
6
|
import { resolveScriptKind } from '../format-checker/scan/utils.js';
|
|
7
|
-
import {
|
|
7
|
+
import { promptDirectorySelection, describeDirectorySelection } from '../directory-picker.js';
|
|
8
|
+
import { runHelperCli } from '../helper-cli.js';
|
|
8
9
|
import { loadRobotSettings } from './config.js';
|
|
9
10
|
import { promptRobotSettings } from './cli/prompts.js';
|
|
10
11
|
import { collectFunctions } from './extractors/functions.js';
|
|
@@ -12,30 +13,70 @@ import { collectComponents } from './extractors/components.js';
|
|
|
12
13
|
import { collectConstants } from './extractors/constants.js';
|
|
13
14
|
import { collectTypes } from './extractors/types.js';
|
|
14
15
|
import { collectClasses } from './extractors/classes.js';
|
|
16
|
+
import { serializeRobotResult } from './serializer.js';
|
|
15
17
|
export async function runRobot() {
|
|
16
18
|
const defaults = await loadRobotSettings();
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
let settings = defaults;
|
|
20
|
+
let exportMode = 'console';
|
|
19
21
|
console.log(colors.magenta('Running robot analyzer (manager.robot settings)'));
|
|
20
|
-
|
|
22
|
+
while (true) {
|
|
23
|
+
let lastAction;
|
|
24
|
+
let runResult;
|
|
25
|
+
const scripts = [
|
|
26
|
+
{
|
|
27
|
+
name: 'Run robot extractor',
|
|
28
|
+
emoji: '🤖',
|
|
29
|
+
description: robotSettingsSummary(settings),
|
|
30
|
+
handler: async () => {
|
|
31
|
+
const selection = await promptDirectorySelection({
|
|
32
|
+
title: 'Select directory for robot extractor',
|
|
33
|
+
});
|
|
34
|
+
if (!selection)
|
|
35
|
+
return;
|
|
36
|
+
lastAction = 'run';
|
|
37
|
+
runResult = await executeRobotExtraction(settings, exportMode, selection.absolutePath, describeDirectorySelection(selection));
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'Adjust settings',
|
|
42
|
+
emoji: '⚙️',
|
|
43
|
+
description: robotSettingsSummary(settings),
|
|
44
|
+
handler: async () => {
|
|
45
|
+
settings = await promptRobotSettings(settings);
|
|
46
|
+
lastAction = 'configure';
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'Change export mode',
|
|
51
|
+
emoji: '📤',
|
|
52
|
+
description: `Current: ${exportMode}`,
|
|
53
|
+
handler: async () => {
|
|
54
|
+
exportMode = await promptExportMode();
|
|
55
|
+
lastAction = 'export';
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
const ran = await runHelperCli({
|
|
60
|
+
title: 'Robot metadata helper',
|
|
61
|
+
scripts,
|
|
62
|
+
argv: [],
|
|
63
|
+
});
|
|
64
|
+
if (!ran)
|
|
65
|
+
return createEmptyRobotResult();
|
|
66
|
+
if (lastAction === 'run' && runResult) {
|
|
67
|
+
return runResult;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function executeRobotExtraction(settings, exportMode, scanRoot, scanLabel) {
|
|
72
|
+
console.log(colors.magenta(`Scanning ${scanLabel} for metadata`));
|
|
73
|
+
const files = await collectSourceFiles(scanRoot);
|
|
21
74
|
if (files.length === 0) {
|
|
22
75
|
console.log(colors.yellow('No source files found for robot'));
|
|
23
|
-
return
|
|
24
|
-
functions: [],
|
|
25
|
-
components: [],
|
|
26
|
-
types: [],
|
|
27
|
-
consts: [],
|
|
28
|
-
classes: [],
|
|
29
|
-
};
|
|
76
|
+
return createEmptyRobotResult();
|
|
30
77
|
}
|
|
31
78
|
console.log(colors.magenta(`Analyzing ${files.length} files for metadata`));
|
|
32
|
-
const result =
|
|
33
|
-
functions: [],
|
|
34
|
-
components: [],
|
|
35
|
-
types: [],
|
|
36
|
-
consts: [],
|
|
37
|
-
classes: [],
|
|
38
|
-
};
|
|
79
|
+
const result = createEmptyRobotResult();
|
|
39
80
|
for (const file of files) {
|
|
40
81
|
let content;
|
|
41
82
|
try {
|
|
@@ -71,27 +112,43 @@ export async function runRobot() {
|
|
|
71
112
|
consts: result.consts.length,
|
|
72
113
|
classes: result.classes.length,
|
|
73
114
|
};
|
|
74
|
-
const
|
|
75
|
-
const tokenEstimate = estimateTokenCount(
|
|
115
|
+
const serializedResult = serializeRobotResult(result, settings);
|
|
116
|
+
const tokenEstimate = estimateTokenCount(serializedResult);
|
|
76
117
|
result.tokenEstimate = tokenEstimate;
|
|
118
|
+
const detailLabel = settings.condenseOutput
|
|
119
|
+
? 'Detailed results (condensed)'
|
|
120
|
+
: 'Detailed results';
|
|
77
121
|
const summaryText = `Robot extraction complete (functions=${summary.functions} components=${summary.components} types=${summary.types} consts=${summary.consts} classes=${summary.classes})`;
|
|
78
122
|
console.log(colors.green(summaryText));
|
|
79
123
|
console.log(colors.dim(`Estimated tokens: ${tokenEstimate}`));
|
|
80
124
|
if (exportMode === 'console') {
|
|
81
|
-
console.log(colors.dim(
|
|
82
|
-
console.log(
|
|
125
|
+
console.log(colors.dim(detailLabel));
|
|
126
|
+
console.log(serializedResult);
|
|
83
127
|
}
|
|
84
128
|
else {
|
|
85
129
|
await exportReportLines('robot-metadata', 'json', [
|
|
86
130
|
summaryText,
|
|
87
131
|
`Token estimate: ${tokenEstimate}`,
|
|
88
132
|
'',
|
|
89
|
-
|
|
90
|
-
|
|
133
|
+
`${detailLabel}:`,
|
|
134
|
+
serializedResult,
|
|
91
135
|
]);
|
|
92
136
|
}
|
|
93
137
|
return result;
|
|
94
138
|
}
|
|
139
|
+
function createEmptyRobotResult() {
|
|
140
|
+
return {
|
|
141
|
+
functions: [],
|
|
142
|
+
components: [],
|
|
143
|
+
types: [],
|
|
144
|
+
consts: [],
|
|
145
|
+
classes: [],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function robotSettingsSummary(settings) {
|
|
149
|
+
const kinds = settings.includeKinds.join(',');
|
|
150
|
+
return `kinds: ${kinds} · exportOnly=${settings.exportedOnly ? 'yes' : 'no'} · columns≤${settings.maxColumns}`;
|
|
151
|
+
}
|
|
95
152
|
function estimateTokenCount(serialized) {
|
|
96
153
|
const length = Math.max(1, serialized.length);
|
|
97
154
|
return Math.ceil(length / 4);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { getDocString, getLocation, isNodeExported,
|
|
2
|
+
import { EMPTY_LOCATION, getDocString, getLocation, isNodeExported, shouldIncludeContext, } from './shared.js';
|
|
3
3
|
export function collectClasses(options) {
|
|
4
4
|
const { sourceFile } = options.context;
|
|
5
5
|
const records = [];
|
|
@@ -10,26 +10,27 @@ export function collectClasses(options) {
|
|
|
10
10
|
}
|
|
11
11
|
const name = node.name?.text;
|
|
12
12
|
const location = getLocation(node, sourceFile);
|
|
13
|
-
|
|
14
|
-
ts.forEachChild(node, visit);
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
13
|
+
const includeContext = shouldIncludeContext(location, options.settings.maxColumns);
|
|
17
14
|
const exported = isNodeExported(node);
|
|
18
15
|
if (options.settings.exportedOnly && !exported) {
|
|
19
16
|
ts.forEachChild(node, visit);
|
|
20
17
|
return;
|
|
21
18
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
|
|
19
|
+
let extendsText;
|
|
20
|
+
let implementsList;
|
|
21
|
+
if (includeContext) {
|
|
22
|
+
const extendsClause = node.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
|
|
23
|
+
const implementsClause = node.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ImplementsKeyword);
|
|
24
|
+
extendsText = extendsClause?.types
|
|
25
|
+
.map((expr) => expr.getText(sourceFile))
|
|
26
|
+
.join(', ');
|
|
27
|
+
implementsList = implementsClause?.types.map((expr) => expr.getText(sourceFile));
|
|
28
|
+
}
|
|
28
29
|
records.push({
|
|
29
30
|
kind: 'class',
|
|
30
31
|
name,
|
|
31
|
-
location,
|
|
32
|
-
docString: getDocString(node),
|
|
32
|
+
location: includeContext ? location : EMPTY_LOCATION,
|
|
33
|
+
docString: includeContext ? getDocString(node) : undefined,
|
|
33
34
|
exported,
|
|
34
35
|
extends: extendsText,
|
|
35
36
|
implements: implementsList,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { describeParameter, getDocString, getFunctionName, getLocation, isComponentFunction, isNodeExported,
|
|
2
|
+
import { describeParameter, EMPTY_LOCATION, getDocString, getFunctionName, getLocation, isComponentFunction, isNodeExported, shouldIncludeContext, } from './shared.js';
|
|
3
3
|
export function collectComponents(options) {
|
|
4
4
|
const { sourceFile } = options.context;
|
|
5
5
|
const records = [];
|
|
@@ -14,24 +14,31 @@ export function collectComponents(options) {
|
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
const location = getLocation(node, sourceFile);
|
|
17
|
-
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
17
|
+
const includeContext = shouldIncludeContext(location, options.settings.maxColumns);
|
|
20
18
|
const exported = isNodeExported(node);
|
|
21
19
|
if (options.settings.exportedOnly && !exported)
|
|
22
20
|
return;
|
|
23
|
-
const inputs =
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
const inputs = includeContext
|
|
22
|
+
? node.parameters.map((param) => describeParameter(param, sourceFile))
|
|
23
|
+
: [];
|
|
24
|
+
const output = includeContext
|
|
25
|
+
? node.type?.getText(sourceFile)
|
|
26
|
+
: undefined;
|
|
27
|
+
const docString = includeContext ? getDocString(node) : undefined;
|
|
28
|
+
let jsxLocation;
|
|
29
|
+
if (includeContext) {
|
|
30
|
+
const jsxNode = findFirstJsx(node);
|
|
31
|
+
jsxLocation = jsxNode ? getLocation(jsxNode, sourceFile) : location;
|
|
32
|
+
}
|
|
26
33
|
records.push({
|
|
27
34
|
kind: 'component',
|
|
28
35
|
name,
|
|
29
|
-
location,
|
|
30
|
-
docString
|
|
36
|
+
location: includeContext ? location : EMPTY_LOCATION,
|
|
37
|
+
docString,
|
|
31
38
|
exported,
|
|
32
39
|
inputs,
|
|
33
40
|
output,
|
|
34
|
-
jsxLocation
|
|
41
|
+
jsxLocation,
|
|
35
42
|
});
|
|
36
43
|
}
|
|
37
44
|
ts.forEachChild(node, visit);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { getDocString, getLocation, isNodeExported,
|
|
2
|
+
import { EMPTY_LOCATION, getDocString, getLocation, isNodeExported, shouldIncludeContext, } from './shared.js';
|
|
3
3
|
export function collectConstants(options) {
|
|
4
4
|
const { sourceFile } = options.context;
|
|
5
5
|
const records = [];
|
|
@@ -23,15 +23,18 @@ export function collectConstants(options) {
|
|
|
23
23
|
if (!ts.isIdentifier(declaration.name))
|
|
24
24
|
return;
|
|
25
25
|
const location = getLocation(declaration.name, sourceFile);
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const includeContext = shouldIncludeContext(location, options.settings.maxColumns);
|
|
27
|
+
const docString = includeContext ? getDocString(declaration) : undefined;
|
|
28
|
+
const value = includeContext
|
|
29
|
+
? declaration.initializer?.getText(sourceFile) ?? 'undefined'
|
|
30
|
+
: '';
|
|
28
31
|
records.push({
|
|
29
32
|
kind: 'const',
|
|
30
33
|
name: declaration.name.text,
|
|
31
|
-
location,
|
|
32
|
-
docString
|
|
34
|
+
location: includeContext ? location : EMPTY_LOCATION,
|
|
35
|
+
docString,
|
|
33
36
|
exported,
|
|
34
|
-
value
|
|
37
|
+
value,
|
|
35
38
|
});
|
|
36
39
|
});
|
|
37
40
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { describeParameter, getDocString, getFunctionName, getLocation, isComponentFunction, isNodeExported,
|
|
2
|
+
import { describeParameter, EMPTY_LOCATION, getDocString, getFunctionName, getLocation, isComponentFunction, isNodeExported, shouldIncludeContext, } from './shared.js';
|
|
3
3
|
export function collectFunctions(options) {
|
|
4
4
|
const { sourceFile } = options.context;
|
|
5
5
|
const records = [];
|
|
@@ -15,19 +15,22 @@ export function collectFunctions(options) {
|
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
17
|
const location = getLocation(node, sourceFile);
|
|
18
|
-
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
18
|
+
const includeContext = shouldIncludeContext(location, options.settings.maxColumns);
|
|
21
19
|
const exported = isNodeExported(node);
|
|
22
20
|
if (options.settings.exportedOnly && !exported)
|
|
23
21
|
return;
|
|
24
|
-
const inputs =
|
|
25
|
-
|
|
22
|
+
const inputs = includeContext
|
|
23
|
+
? node.parameters.map((param) => describeParameter(param, sourceFile))
|
|
24
|
+
: [];
|
|
25
|
+
const output = includeContext
|
|
26
|
+
? node.type?.getText(sourceFile)
|
|
27
|
+
: undefined;
|
|
28
|
+
const docString = includeContext ? getDocString(node) : undefined;
|
|
26
29
|
records.push({
|
|
27
30
|
kind: 'function',
|
|
28
31
|
name,
|
|
29
|
-
location,
|
|
30
|
-
docString
|
|
32
|
+
location: includeContext ? location : EMPTY_LOCATION,
|
|
33
|
+
docString,
|
|
31
34
|
exported,
|
|
32
35
|
inputs,
|
|
33
36
|
output,
|
|
@@ -44,11 +44,16 @@ export function isNodeExported(node) {
|
|
|
44
44
|
return true;
|
|
45
45
|
return false;
|
|
46
46
|
}
|
|
47
|
-
export function
|
|
47
|
+
export function shouldIncludeContext(location, maxColumns) {
|
|
48
48
|
if (maxColumns <= 0)
|
|
49
49
|
return true;
|
|
50
50
|
return location.column <= maxColumns;
|
|
51
51
|
}
|
|
52
|
+
export const EMPTY_LOCATION = {
|
|
53
|
+
file: '',
|
|
54
|
+
line: 0,
|
|
55
|
+
column: 0,
|
|
56
|
+
};
|
|
52
57
|
export function getFunctionName(node) {
|
|
53
58
|
if ('name' in node && node.name && ts.isIdentifier(node.name)) {
|
|
54
59
|
return node.name.text;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { getDocString, getLocation, isNodeExported,
|
|
2
|
+
import { EMPTY_LOCATION, getDocString, getLocation, isNodeExported, shouldIncludeContext, } from './shared.js';
|
|
3
3
|
export function collectTypes(options) {
|
|
4
4
|
const { sourceFile } = options.context;
|
|
5
5
|
const records = [];
|
|
@@ -16,10 +16,7 @@ export function collectTypes(options) {
|
|
|
16
16
|
}
|
|
17
17
|
if (typeKind) {
|
|
18
18
|
const location = getLocation(node, sourceFile);
|
|
19
|
-
|
|
20
|
-
ts.forEachChild(node, visit);
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
19
|
+
const includeContext = shouldIncludeContext(location, options.settings.maxColumns);
|
|
23
20
|
const exported = isNodeExported(node);
|
|
24
21
|
if (options.settings.exportedOnly && !exported) {
|
|
25
22
|
ts.forEachChild(node, visit);
|
|
@@ -29,11 +26,11 @@ export function collectTypes(options) {
|
|
|
29
26
|
records.push({
|
|
30
27
|
kind: 'type',
|
|
31
28
|
name,
|
|
32
|
-
location,
|
|
33
|
-
docString: getDocString(node),
|
|
29
|
+
location: includeContext ? location : EMPTY_LOCATION,
|
|
30
|
+
docString: includeContext ? getDocString(node) : undefined,
|
|
34
31
|
exported,
|
|
35
32
|
typeKind,
|
|
36
|
-
definition: node.getText(sourceFile),
|
|
33
|
+
definition: includeContext ? node.getText(sourceFile) : '',
|
|
37
34
|
});
|
|
38
35
|
}
|
|
39
36
|
ts.forEachChild(node, visit);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const CONDENSED_FIELDS = [
|
|
2
|
+
'kind',
|
|
3
|
+
'file',
|
|
4
|
+
'line',
|
|
5
|
+
'column',
|
|
6
|
+
'name',
|
|
7
|
+
'signature',
|
|
8
|
+
'docString',
|
|
9
|
+
'exported',
|
|
10
|
+
];
|
|
11
|
+
export function serializeRobotResult(result, settings) {
|
|
12
|
+
if (!settings.condenseOutput) {
|
|
13
|
+
return JSON.stringify(result, null, 2);
|
|
14
|
+
}
|
|
15
|
+
return JSON.stringify(buildCondensedPayload(result, settings));
|
|
16
|
+
}
|
|
17
|
+
function buildCondensedPayload(result, settings) {
|
|
18
|
+
const rows = [];
|
|
19
|
+
const limit = settings.maxDocStringLength;
|
|
20
|
+
for (const record of flattenRecords(result)) {
|
|
21
|
+
rows.push([
|
|
22
|
+
record.kind,
|
|
23
|
+
record.location.file,
|
|
24
|
+
record.location.line,
|
|
25
|
+
record.location.column,
|
|
26
|
+
record.name ?? '',
|
|
27
|
+
buildSignature(record),
|
|
28
|
+
trimDocString(record.docString, limit),
|
|
29
|
+
record.exported,
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
version: 1,
|
|
34
|
+
summary: {
|
|
35
|
+
functions: result.functions.length,
|
|
36
|
+
components: result.components.length,
|
|
37
|
+
types: result.types.length,
|
|
38
|
+
consts: result.consts.length,
|
|
39
|
+
classes: result.classes.length,
|
|
40
|
+
},
|
|
41
|
+
fields: [...CONDENSED_FIELDS],
|
|
42
|
+
rows,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function flattenRecords(result) {
|
|
46
|
+
return [
|
|
47
|
+
...result.functions,
|
|
48
|
+
...result.components,
|
|
49
|
+
...result.types,
|
|
50
|
+
...result.consts,
|
|
51
|
+
...result.classes,
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
function buildSignature(record) {
|
|
55
|
+
switch (record.kind) {
|
|
56
|
+
case 'function':
|
|
57
|
+
case 'component': {
|
|
58
|
+
const params = record.inputs.join(', ');
|
|
59
|
+
const output = record.output ? ` => ${normalize(record.output)}` : '';
|
|
60
|
+
return `(${params})${output}`;
|
|
61
|
+
}
|
|
62
|
+
case 'type': {
|
|
63
|
+
const displayKind = record.typeKind === 'type-alias'
|
|
64
|
+
? 'type alias'
|
|
65
|
+
: record.typeKind;
|
|
66
|
+
return normalize(`${displayKind} ${record.definition}`);
|
|
67
|
+
}
|
|
68
|
+
case 'const':
|
|
69
|
+
return normalize(record.value);
|
|
70
|
+
case 'class': {
|
|
71
|
+
const parts = [];
|
|
72
|
+
if (record.extends)
|
|
73
|
+
parts.push(`extends ${record.extends}`);
|
|
74
|
+
if (record.implements?.length) {
|
|
75
|
+
parts.push(`implements ${record.implements.join(', ')}`);
|
|
76
|
+
}
|
|
77
|
+
return normalize(parts.join(' '));
|
|
78
|
+
}
|
|
79
|
+
default:
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function trimDocString(value, limit) {
|
|
84
|
+
if (!value) {
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
const normalized = normalize(value);
|
|
88
|
+
if (limit <= 0 || normalized.length <= limit) {
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
return `${normalized.slice(0, limit)}...`;
|
|
92
|
+
}
|
|
93
|
+
function normalize(value) {
|
|
94
|
+
return (value ?? '')
|
|
95
|
+
.replace(/\s+/g, ' ')
|
|
96
|
+
.trim();
|
|
97
|
+
}
|