tokenlean 0.1.0
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 +248 -0
- package/bin/tl-api.mjs +515 -0
- package/bin/tl-blame.mjs +345 -0
- package/bin/tl-complexity.mjs +514 -0
- package/bin/tl-component.mjs +274 -0
- package/bin/tl-config.mjs +135 -0
- package/bin/tl-context.mjs +156 -0
- package/bin/tl-coverage.mjs +456 -0
- package/bin/tl-deps.mjs +474 -0
- package/bin/tl-diff.mjs +183 -0
- package/bin/tl-entry.mjs +256 -0
- package/bin/tl-env.mjs +376 -0
- package/bin/tl-exports.mjs +583 -0
- package/bin/tl-flow.mjs +324 -0
- package/bin/tl-history.mjs +289 -0
- package/bin/tl-hotspots.mjs +321 -0
- package/bin/tl-impact.mjs +345 -0
- package/bin/tl-prompt.mjs +175 -0
- package/bin/tl-related.mjs +227 -0
- package/bin/tl-routes.mjs +627 -0
- package/bin/tl-search.mjs +123 -0
- package/bin/tl-structure.mjs +161 -0
- package/bin/tl-symbols.mjs +430 -0
- package/bin/tl-todo.mjs +341 -0
- package/bin/tl-types.mjs +441 -0
- package/bin/tl-unused.mjs +494 -0
- package/package.json +55 -0
- package/src/config.mjs +271 -0
- package/src/output.mjs +251 -0
- package/src/project.mjs +277 -0
package/bin/tl-types.mjs
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-types - Extract TypeScript types, interfaces, and enums with full definitions
|
|
5
|
+
*
|
|
6
|
+
* Unlike tl-symbols (which shows signatures only), this extracts complete type
|
|
7
|
+
* definitions including all properties. Perfect for understanding data shapes
|
|
8
|
+
* without reading implementation code.
|
|
9
|
+
*
|
|
10
|
+
* Usage: tl-types <file-or-dir> [--exports-only]
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Prompt info for tl-prompt
|
|
14
|
+
if (process.argv.includes('--prompt')) {
|
|
15
|
+
console.log(JSON.stringify({
|
|
16
|
+
name: 'tl-types',
|
|
17
|
+
desc: 'Extract full TypeScript type definitions',
|
|
18
|
+
when: 'before-read',
|
|
19
|
+
example: 'tl-types src/types/'
|
|
20
|
+
}));
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
25
|
+
import { basename, extname, join, relative } from 'path';
|
|
26
|
+
import {
|
|
27
|
+
createOutput,
|
|
28
|
+
parseCommonArgs,
|
|
29
|
+
estimateTokens,
|
|
30
|
+
formatTokens,
|
|
31
|
+
COMMON_OPTIONS_HELP
|
|
32
|
+
} from '../src/output.mjs';
|
|
33
|
+
import { findProjectRoot, shouldSkip } from '../src/project.mjs';
|
|
34
|
+
|
|
35
|
+
const HELP = `
|
|
36
|
+
tl-types - Extract TypeScript types, interfaces, and enums with full definitions
|
|
37
|
+
|
|
38
|
+
Usage: tl-types <file-or-dir> [options]
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
--exports-only, -e Show only exported types
|
|
42
|
+
--no-comments Strip comments from type definitions
|
|
43
|
+
--flat Don't show file headers (for single output)
|
|
44
|
+
${COMMON_OPTIONS_HELP}
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
tl-types src/types.ts # All types from file
|
|
48
|
+
tl-types src/types/ # All types from directory
|
|
49
|
+
tl-types src/ -e # Exported types only
|
|
50
|
+
tl-types src/api.ts -l 50 # Limit to 50 lines
|
|
51
|
+
tl-types src/ -j # JSON output
|
|
52
|
+
|
|
53
|
+
Extracts:
|
|
54
|
+
- interface definitions (with all properties)
|
|
55
|
+
- type aliases (full definition)
|
|
56
|
+
- enum definitions (with all values)
|
|
57
|
+
- Generic type parameters
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
// ─────────────────────────────────────────────────────────────
|
|
61
|
+
// TypeScript Type Extraction
|
|
62
|
+
// ─────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function extractTypes(content, options = {}) {
|
|
65
|
+
const { exportsOnly = false, stripComments = false } = options;
|
|
66
|
+
const types = {
|
|
67
|
+
interfaces: [],
|
|
68
|
+
typeAliases: [],
|
|
69
|
+
enums: []
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const lines = content.split('\n');
|
|
73
|
+
let i = 0;
|
|
74
|
+
|
|
75
|
+
while (i < lines.length) {
|
|
76
|
+
const line = lines[i];
|
|
77
|
+
const trimmed = line.trim();
|
|
78
|
+
|
|
79
|
+
// Skip if exports only and not exported
|
|
80
|
+
const isExported = trimmed.startsWith('export ');
|
|
81
|
+
if (exportsOnly && !isExported) {
|
|
82
|
+
i++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Interface
|
|
87
|
+
const interfaceMatch = trimmed.match(/^(export\s+)?(interface)\s+(\w+)(\s*<[^>]+>)?(\s+extends\s+[^{]+)?/);
|
|
88
|
+
if (interfaceMatch) {
|
|
89
|
+
const extracted = extractBlock(lines, i, stripComments);
|
|
90
|
+
types.interfaces.push({
|
|
91
|
+
name: interfaceMatch[3],
|
|
92
|
+
exported: !!interfaceMatch[1],
|
|
93
|
+
definition: extracted.content
|
|
94
|
+
});
|
|
95
|
+
i = extracted.endLine + 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Type alias
|
|
100
|
+
const typeMatch = trimmed.match(/^(export\s+)?type\s+(\w+)(\s*<[^>]+>)?\s*=/);
|
|
101
|
+
if (typeMatch) {
|
|
102
|
+
const extracted = extractTypeAlias(lines, i, stripComments);
|
|
103
|
+
types.typeAliases.push({
|
|
104
|
+
name: typeMatch[2],
|
|
105
|
+
exported: !!typeMatch[1],
|
|
106
|
+
definition: extracted.content
|
|
107
|
+
});
|
|
108
|
+
i = extracted.endLine + 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Enum
|
|
113
|
+
const enumMatch = trimmed.match(/^(export\s+)?(const\s+)?enum\s+(\w+)/);
|
|
114
|
+
if (enumMatch) {
|
|
115
|
+
const extracted = extractBlock(lines, i, stripComments);
|
|
116
|
+
types.enums.push({
|
|
117
|
+
name: enumMatch[3],
|
|
118
|
+
exported: !!enumMatch[1],
|
|
119
|
+
isConst: !!enumMatch[2],
|
|
120
|
+
definition: extracted.content
|
|
121
|
+
});
|
|
122
|
+
i = extracted.endLine + 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
i++;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return types;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractBlock(lines, startLine, stripComments) {
|
|
133
|
+
let content = [];
|
|
134
|
+
let braceDepth = 0;
|
|
135
|
+
let started = false;
|
|
136
|
+
let i = startLine;
|
|
137
|
+
|
|
138
|
+
// Collect leading comments if not stripping
|
|
139
|
+
if (!stripComments) {
|
|
140
|
+
let commentStart = startLine;
|
|
141
|
+
while (commentStart > 0) {
|
|
142
|
+
const prevLine = lines[commentStart - 1].trim();
|
|
143
|
+
if (prevLine.startsWith('//') || prevLine.startsWith('*') || prevLine.startsWith('/*') || prevLine === '*/') {
|
|
144
|
+
commentStart--;
|
|
145
|
+
} else if (prevLine === '') {
|
|
146
|
+
// Check if there's a comment block above the empty line
|
|
147
|
+
if (commentStart > 1 && lines[commentStart - 2].trim().startsWith('*/')) {
|
|
148
|
+
commentStart--;
|
|
149
|
+
} else {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Add comments
|
|
157
|
+
for (let j = commentStart; j < startLine; j++) {
|
|
158
|
+
content.push(lines[j]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
while (i < lines.length) {
|
|
163
|
+
const line = lines[i];
|
|
164
|
+
const trimmed = line.trim();
|
|
165
|
+
|
|
166
|
+
// Skip comments if stripping
|
|
167
|
+
if (stripComments && (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'))) {
|
|
168
|
+
i++;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
content.push(line);
|
|
173
|
+
|
|
174
|
+
// Count braces
|
|
175
|
+
for (const char of line) {
|
|
176
|
+
if (char === '{') {
|
|
177
|
+
braceDepth++;
|
|
178
|
+
started = true;
|
|
179
|
+
} else if (char === '}') {
|
|
180
|
+
braceDepth--;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// End when we close the opening brace
|
|
185
|
+
if (started && braceDepth === 0) {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
i++;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
content: content.join('\n'),
|
|
194
|
+
endLine: i
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function extractTypeAlias(lines, startLine, stripComments) {
|
|
199
|
+
let content = [];
|
|
200
|
+
let i = startLine;
|
|
201
|
+
let braceDepth = 0;
|
|
202
|
+
let parenDepth = 0;
|
|
203
|
+
let angleBracketDepth = 0;
|
|
204
|
+
|
|
205
|
+
// Collect leading comments if not stripping
|
|
206
|
+
if (!stripComments) {
|
|
207
|
+
let commentStart = startLine;
|
|
208
|
+
while (commentStart > 0) {
|
|
209
|
+
const prevLine = lines[commentStart - 1].trim();
|
|
210
|
+
if (prevLine.startsWith('//') || prevLine.startsWith('*') || prevLine.startsWith('/*') || prevLine === '*/') {
|
|
211
|
+
commentStart--;
|
|
212
|
+
} else if (prevLine === '') {
|
|
213
|
+
if (commentStart > 1 && lines[commentStart - 2].trim().startsWith('*/')) {
|
|
214
|
+
commentStart--;
|
|
215
|
+
} else {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (let j = commentStart; j < startLine; j++) {
|
|
223
|
+
content.push(lines[j]);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
while (i < lines.length) {
|
|
228
|
+
const line = lines[i];
|
|
229
|
+
const trimmed = line.trim();
|
|
230
|
+
|
|
231
|
+
// Skip comments if stripping
|
|
232
|
+
if (stripComments && (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'))) {
|
|
233
|
+
i++;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
content.push(line);
|
|
238
|
+
|
|
239
|
+
// Track nesting
|
|
240
|
+
for (const char of line) {
|
|
241
|
+
if (char === '{') braceDepth++;
|
|
242
|
+
else if (char === '}') braceDepth--;
|
|
243
|
+
else if (char === '(') parenDepth++;
|
|
244
|
+
else if (char === ')') parenDepth--;
|
|
245
|
+
else if (char === '<') angleBracketDepth++;
|
|
246
|
+
else if (char === '>') angleBracketDepth--;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Type alias ends with semicolon or newline when all brackets closed
|
|
250
|
+
if (braceDepth === 0 && parenDepth === 0 && angleBracketDepth <= 0) {
|
|
251
|
+
if (trimmed.endsWith(';') || trimmed.endsWith('}') || trimmed.endsWith(')') || trimmed.endsWith('>')) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
// Check if next line is a new statement
|
|
255
|
+
if (i + 1 < lines.length) {
|
|
256
|
+
const nextTrimmed = lines[i + 1].trim();
|
|
257
|
+
if (nextTrimmed.startsWith('export ') || nextTrimmed.startsWith('interface ') ||
|
|
258
|
+
nextTrimmed.startsWith('type ') || nextTrimmed.startsWith('enum ') ||
|
|
259
|
+
nextTrimmed.startsWith('const ') || nextTrimmed.startsWith('function ') ||
|
|
260
|
+
nextTrimmed.startsWith('class ') || nextTrimmed === '') {
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
i++;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
content: content.join('\n'),
|
|
271
|
+
endLine: i
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ─────────────────────────────────────────────────────────────
|
|
276
|
+
// File Discovery
|
|
277
|
+
// ─────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
const TS_EXTENSIONS = new Set(['.ts', '.tsx', '.mts']);
|
|
280
|
+
|
|
281
|
+
function isTypeScriptFile(filePath) {
|
|
282
|
+
return TS_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function findTypeScriptFiles(dir, files = []) {
|
|
286
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
287
|
+
|
|
288
|
+
for (const entry of entries) {
|
|
289
|
+
const fullPath = join(dir, entry.name);
|
|
290
|
+
|
|
291
|
+
if (entry.isDirectory()) {
|
|
292
|
+
if (!shouldSkip(entry.name, true)) {
|
|
293
|
+
findTypeScriptFiles(fullPath, files);
|
|
294
|
+
}
|
|
295
|
+
} else if (entry.isFile() && isTypeScriptFile(entry.name)) {
|
|
296
|
+
if (!shouldSkip(entry.name, false)) {
|
|
297
|
+
files.push(fullPath);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return files;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ─────────────────────────────────────────────────────────────
|
|
306
|
+
// Formatting
|
|
307
|
+
// ─────────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
function formatTypes(types, out, showFileHeader = true, filePath = '') {
|
|
310
|
+
const hasContent = types.interfaces.length > 0 ||
|
|
311
|
+
types.typeAliases.length > 0 ||
|
|
312
|
+
types.enums.length > 0;
|
|
313
|
+
|
|
314
|
+
if (!hasContent) return 0;
|
|
315
|
+
|
|
316
|
+
let count = 0;
|
|
317
|
+
|
|
318
|
+
if (types.interfaces.length > 0) {
|
|
319
|
+
if (!showFileHeader) out.add('// Interfaces');
|
|
320
|
+
for (const iface of types.interfaces) {
|
|
321
|
+
out.add(iface.definition);
|
|
322
|
+
out.blank();
|
|
323
|
+
count++;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (types.typeAliases.length > 0) {
|
|
328
|
+
if (!showFileHeader) out.add('// Type Aliases');
|
|
329
|
+
for (const alias of types.typeAliases) {
|
|
330
|
+
out.add(alias.definition);
|
|
331
|
+
out.blank();
|
|
332
|
+
count++;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (types.enums.length > 0) {
|
|
337
|
+
if (!showFileHeader) out.add('// Enums');
|
|
338
|
+
for (const enumDef of types.enums) {
|
|
339
|
+
out.add(enumDef.definition);
|
|
340
|
+
out.blank();
|
|
341
|
+
count++;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return count;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function countTypes(types) {
|
|
349
|
+
return types.interfaces.length + types.typeAliases.length + types.enums.length;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ─────────────────────────────────────────────────────────────
|
|
353
|
+
// Main
|
|
354
|
+
// ─────────────────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
const args = process.argv.slice(2);
|
|
357
|
+
const options = parseCommonArgs(args);
|
|
358
|
+
const exportsOnly = options.remaining.includes('--exports-only') || options.remaining.includes('-e');
|
|
359
|
+
const stripComments = options.remaining.includes('--no-comments');
|
|
360
|
+
const flat = options.remaining.includes('--flat');
|
|
361
|
+
const targetPath = options.remaining.find(a => !a.startsWith('-'));
|
|
362
|
+
|
|
363
|
+
if (options.help || !targetPath) {
|
|
364
|
+
console.log(HELP);
|
|
365
|
+
process.exit(options.help ? 0 : 1);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!existsSync(targetPath)) {
|
|
369
|
+
console.error(`Path not found: ${targetPath}`);
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const projectRoot = findProjectRoot();
|
|
374
|
+
const out = createOutput(options);
|
|
375
|
+
const allTypes = [];
|
|
376
|
+
let totalTypes = 0;
|
|
377
|
+
let totalTokens = 0;
|
|
378
|
+
|
|
379
|
+
const stat = statSync(targetPath);
|
|
380
|
+
const files = stat.isDirectory()
|
|
381
|
+
? findTypeScriptFiles(targetPath)
|
|
382
|
+
: isTypeScriptFile(targetPath) ? [targetPath] : [];
|
|
383
|
+
|
|
384
|
+
if (files.length === 0) {
|
|
385
|
+
console.error('No TypeScript files found');
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const showFileHeaders = files.length > 1 && !flat;
|
|
390
|
+
|
|
391
|
+
for (const filePath of files) {
|
|
392
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
393
|
+
const types = extractTypes(content, { exportsOnly, stripComments });
|
|
394
|
+
const count = countTypes(types);
|
|
395
|
+
|
|
396
|
+
if (count === 0) continue;
|
|
397
|
+
|
|
398
|
+
const relPath = relative(projectRoot, filePath);
|
|
399
|
+
totalTypes += count;
|
|
400
|
+
|
|
401
|
+
// Add file header
|
|
402
|
+
if (showFileHeaders) {
|
|
403
|
+
out.add(`// ═══════════════════════════════════════════════════════════`);
|
|
404
|
+
out.add(`// ${relPath} (${count} types)`);
|
|
405
|
+
out.add(`// ═══════════════════════════════════════════════════════════`);
|
|
406
|
+
out.blank();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
formatTypes(types, out, showFileHeaders, filePath);
|
|
410
|
+
|
|
411
|
+
// Collect for JSON
|
|
412
|
+
allTypes.push({
|
|
413
|
+
file: relPath,
|
|
414
|
+
...types
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Calculate token savings
|
|
419
|
+
const outputText = out.render();
|
|
420
|
+
totalTokens = estimateTokens(outputText);
|
|
421
|
+
|
|
422
|
+
// For comparison, estimate full file tokens
|
|
423
|
+
let fullFileTokens = 0;
|
|
424
|
+
for (const filePath of files) {
|
|
425
|
+
fullFileTokens += estimateTokens(readFileSync(filePath, 'utf-8'));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Set JSON data
|
|
429
|
+
out.setData('files', allTypes);
|
|
430
|
+
out.setData('totalTypes', totalTypes);
|
|
431
|
+
out.setData('totalTokens', totalTokens);
|
|
432
|
+
out.setData('fullFileTokens', fullFileTokens);
|
|
433
|
+
|
|
434
|
+
// Add summary
|
|
435
|
+
if (!options.quiet && totalTypes > 0) {
|
|
436
|
+
out.add(`// ───────────────────────────────────────────────────────────`);
|
|
437
|
+
out.add(`// Summary: ${totalTypes} types from ${files.length} file(s)`);
|
|
438
|
+
out.add(`// Tokens: ~${formatTokens(totalTokens)} (full files: ~${formatTokens(fullFileTokens)})`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
out.print();
|