skrypt-ai 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/LICENSE +21 -0
- package/README.md +200 -0
- package/dist/autofix/index.d.ts +46 -0
- package/dist/autofix/index.js +240 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +40 -0
- package/dist/commands/autofix.d.ts +2 -0
- package/dist/commands/autofix.js +143 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +320 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +56 -0
- package/dist/commands/review-pr.d.ts +2 -0
- package/dist/commands/review-pr.js +117 -0
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +142 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +2 -0
- package/dist/config/loader.d.ts +9 -0
- package/dist/config/loader.js +82 -0
- package/dist/config/types.d.ts +24 -0
- package/dist/config/types.js +34 -0
- package/dist/generator/generator.d.ts +15 -0
- package/dist/generator/generator.js +144 -0
- package/dist/generator/index.d.ts +4 -0
- package/dist/generator/index.js +4 -0
- package/dist/generator/organizer.d.ts +29 -0
- package/dist/generator/organizer.js +222 -0
- package/dist/generator/types.d.ts +83 -0
- package/dist/generator/types.js +1 -0
- package/dist/generator/writer.d.ts +28 -0
- package/dist/generator/writer.js +320 -0
- package/dist/github/pr-comments.d.ts +40 -0
- package/dist/github/pr-comments.js +308 -0
- package/dist/llm/anthropic-client.d.ts +16 -0
- package/dist/llm/anthropic-client.js +92 -0
- package/dist/llm/index.d.ts +53 -0
- package/dist/llm/index.js +400 -0
- package/dist/llm/llm.manual-test.d.ts +1 -0
- package/dist/llm/llm.manual-test.js +112 -0
- package/dist/llm/llm.mock-test.d.ts +4 -0
- package/dist/llm/llm.mock-test.js +79 -0
- package/dist/llm/openai-client.d.ts +17 -0
- package/dist/llm/openai-client.js +90 -0
- package/dist/llm/types.d.ts +60 -0
- package/dist/llm/types.js +20 -0
- package/dist/scanner/content-type.d.ts +39 -0
- package/dist/scanner/content-type.js +194 -0
- package/dist/scanner/content-type.test.d.ts +1 -0
- package/dist/scanner/content-type.test.js +231 -0
- package/dist/scanner/go.d.ts +20 -0
- package/dist/scanner/go.js +269 -0
- package/dist/scanner/index.d.ts +21 -0
- package/dist/scanner/index.js +137 -0
- package/dist/scanner/python.d.ts +6 -0
- package/dist/scanner/python.js +57 -0
- package/dist/scanner/python_parser.py +230 -0
- package/dist/scanner/rust.d.ts +23 -0
- package/dist/scanner/rust.js +304 -0
- package/dist/scanner/scanner.test.d.ts +1 -0
- package/dist/scanner/scanner.test.js +210 -0
- package/dist/scanner/types.d.ts +50 -0
- package/dist/scanner/types.js +1 -0
- package/dist/scanner/typescript.d.ts +34 -0
- package/dist/scanner/typescript.js +327 -0
- package/dist/scanner/typescript.manual-test.d.ts +1 -0
- package/dist/scanner/typescript.manual-test.js +112 -0
- package/dist/template/docs.json +32 -0
- package/dist/template/mdx-components.tsx +62 -0
- package/dist/template/next-env.d.ts +6 -0
- package/dist/template/next.config.mjs +17 -0
- package/dist/template/package.json +39 -0
- package/dist/template/postcss.config.mjs +5 -0
- package/dist/template/public/search-index.json +1 -0
- package/dist/template/scripts/build-search-index.mjs +120 -0
- package/dist/template/src/app/api/mock/[...path]/route.ts +224 -0
- package/dist/template/src/app/api/openapi/route.ts +48 -0
- package/dist/template/src/app/api/rate-limit/route.ts +84 -0
- package/dist/template/src/app/docs/[...slug]/page.tsx +81 -0
- package/dist/template/src/app/docs/layout.tsx +9 -0
- package/dist/template/src/app/docs/page.mdx +67 -0
- package/dist/template/src/app/error.tsx +63 -0
- package/dist/template/src/app/layout.tsx +71 -0
- package/dist/template/src/app/page.tsx +18 -0
- package/dist/template/src/app/reference/route.ts +36 -0
- package/dist/template/src/app/robots.ts +14 -0
- package/dist/template/src/app/sitemap.ts +64 -0
- package/dist/template/src/components/breadcrumbs.tsx +41 -0
- package/dist/template/src/components/copy-button.tsx +29 -0
- package/dist/template/src/components/docs-layout.tsx +35 -0
- package/dist/template/src/components/edit-link.tsx +39 -0
- package/dist/template/src/components/feedback.tsx +52 -0
- package/dist/template/src/components/header.tsx +66 -0
- package/dist/template/src/components/mdx/accordion.tsx +48 -0
- package/dist/template/src/components/mdx/api-badge.tsx +57 -0
- package/dist/template/src/components/mdx/callout.tsx +111 -0
- package/dist/template/src/components/mdx/card.tsx +62 -0
- package/dist/template/src/components/mdx/changelog.tsx +57 -0
- package/dist/template/src/components/mdx/code-block.tsx +42 -0
- package/dist/template/src/components/mdx/code-group.tsx +125 -0
- package/dist/template/src/components/mdx/code-playground.tsx +322 -0
- package/dist/template/src/components/mdx/go-playground.tsx +235 -0
- package/dist/template/src/components/mdx/heading.tsx +37 -0
- package/dist/template/src/components/mdx/highlighted-code.tsx +89 -0
- package/dist/template/src/components/mdx/index.tsx +15 -0
- package/dist/template/src/components/mdx/param-table.tsx +71 -0
- package/dist/template/src/components/mdx/python-playground.tsx +293 -0
- package/dist/template/src/components/mdx/steps.tsx +43 -0
- package/dist/template/src/components/mdx/tabs.tsx +81 -0
- package/dist/template/src/components/rate-limit-display.tsx +183 -0
- package/dist/template/src/components/search-dialog.tsx +178 -0
- package/dist/template/src/components/sidebar.tsx +129 -0
- package/dist/template/src/components/syntax-theme-selector.tsx +50 -0
- package/dist/template/src/components/table-of-contents.tsx +84 -0
- package/dist/template/src/components/theme-toggle.tsx +46 -0
- package/dist/template/src/components/version-selector.tsx +61 -0
- package/dist/template/src/contexts/syntax-theme.tsx +52 -0
- package/dist/template/src/lib/highlight.ts +83 -0
- package/dist/template/src/lib/search-types.ts +37 -0
- package/dist/template/src/lib/search.ts +125 -0
- package/dist/template/src/lib/utils.ts +6 -0
- package/dist/template/src/styles/globals.css +152 -0
- package/dist/template/tsconfig.json +25 -0
- package/dist/template/tsconfig.tsbuildinfo +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { scanFile, scanDirectory } from './index.js';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
const TESTDATA = join(process.cwd(), 'testdata');
|
|
5
|
+
describe('Scanner', () => {
|
|
6
|
+
describe('scanFile', () => {
|
|
7
|
+
describe('Python files', () => {
|
|
8
|
+
it('should scan Python file and extract elements', async () => {
|
|
9
|
+
const result = await scanFile(join(TESTDATA, 'sample.py'));
|
|
10
|
+
expect(result.errors).toHaveLength(0);
|
|
11
|
+
expect(result.language).toBe('python');
|
|
12
|
+
expect(result.elements.length).toBeGreaterThan(0);
|
|
13
|
+
});
|
|
14
|
+
it('should find expected Python functions', async () => {
|
|
15
|
+
const result = await scanFile(join(TESTDATA, 'sample.py'));
|
|
16
|
+
const names = result.elements.map(e => e.name);
|
|
17
|
+
expect(names).toContain('greet');
|
|
18
|
+
expect(names).toContain('fetch_data');
|
|
19
|
+
});
|
|
20
|
+
it('should find Python class and methods', async () => {
|
|
21
|
+
const result = await scanFile(join(TESTDATA, 'sample.py'));
|
|
22
|
+
const names = result.elements.map(e => e.name);
|
|
23
|
+
expect(names).toContain('Calculator');
|
|
24
|
+
expect(names).toContain('add');
|
|
25
|
+
expect(names).toContain('multiply');
|
|
26
|
+
});
|
|
27
|
+
it('should exclude private Python elements', async () => {
|
|
28
|
+
const result = await scanFile(join(TESTDATA, 'sample.py'));
|
|
29
|
+
const names = result.elements.map(e => e.name);
|
|
30
|
+
expect(names).not.toContain('_private_method');
|
|
31
|
+
expect(names).not.toContain('_private_function');
|
|
32
|
+
expect(names).not.toContain('_PrivateClass');
|
|
33
|
+
});
|
|
34
|
+
it('should detect async Python functions', async () => {
|
|
35
|
+
const result = await scanFile(join(TESTDATA, 'sample.py'));
|
|
36
|
+
const fetchData = result.elements.find(e => e.name === 'fetch_data');
|
|
37
|
+
expect(fetchData).toBeDefined();
|
|
38
|
+
expect(fetchData?.isAsync).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
it('should extract Python function parameters', async () => {
|
|
41
|
+
const result = await scanFile(join(TESTDATA, 'sample.py'));
|
|
42
|
+
const greet = result.elements.find(e => e.name === 'greet');
|
|
43
|
+
expect(greet).toBeDefined();
|
|
44
|
+
expect(greet?.parameters).toHaveLength(2);
|
|
45
|
+
expect(greet?.parameters[0].name).toBe('name');
|
|
46
|
+
expect(greet?.parameters[0].type).toBe('str');
|
|
47
|
+
});
|
|
48
|
+
it('should extract Python docstrings', async () => {
|
|
49
|
+
const result = await scanFile(join(TESTDATA, 'sample.py'));
|
|
50
|
+
const greet = result.elements.find(e => e.name === 'greet');
|
|
51
|
+
expect(greet?.docstring).toContain('Generate a greeting');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('TypeScript files', () => {
|
|
55
|
+
it('should scan TypeScript file and extract elements', async () => {
|
|
56
|
+
const result = await scanFile(join(TESTDATA, 'sample.ts'));
|
|
57
|
+
expect(result.errors).toHaveLength(0);
|
|
58
|
+
expect(result.language).toBe('typescript');
|
|
59
|
+
expect(result.elements.length).toBeGreaterThan(0);
|
|
60
|
+
});
|
|
61
|
+
it('should find expected TypeScript functions', async () => {
|
|
62
|
+
const result = await scanFile(join(TESTDATA, 'sample.ts'));
|
|
63
|
+
const names = result.elements.map(e => e.name);
|
|
64
|
+
expect(names).toContain('greet');
|
|
65
|
+
expect(names).toContain('fetchData');
|
|
66
|
+
expect(names).toContain('processItems');
|
|
67
|
+
});
|
|
68
|
+
it('should find TypeScript class and methods', async () => {
|
|
69
|
+
const result = await scanFile(join(TESTDATA, 'sample.ts'));
|
|
70
|
+
const names = result.elements.map(e => e.name);
|
|
71
|
+
expect(names).toContain('Calculator');
|
|
72
|
+
expect(names).toContain('add');
|
|
73
|
+
expect(names).toContain('multiply');
|
|
74
|
+
});
|
|
75
|
+
it('should exclude non-exported TypeScript elements', async () => {
|
|
76
|
+
const result = await scanFile(join(TESTDATA, 'sample.ts'));
|
|
77
|
+
const names = result.elements.map(e => e.name);
|
|
78
|
+
expect(names).not.toContain('internalHelper');
|
|
79
|
+
expect(names).not.toContain('_privateFunction');
|
|
80
|
+
});
|
|
81
|
+
it('should detect async TypeScript functions', async () => {
|
|
82
|
+
const result = await scanFile(join(TESTDATA, 'sample.ts'));
|
|
83
|
+
const fetchData = result.elements.find(e => e.name === 'fetchData');
|
|
84
|
+
const processItems = result.elements.find(e => e.name === 'processItems');
|
|
85
|
+
expect(fetchData?.isAsync).toBe(true);
|
|
86
|
+
expect(processItems?.isAsync).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
it('should set parentClass for methods', async () => {
|
|
89
|
+
const result = await scanFile(join(TESTDATA, 'sample.ts'));
|
|
90
|
+
const addMethod = result.elements.find(e => e.name === 'add');
|
|
91
|
+
expect(addMethod?.parentClass).toBe('Calculator');
|
|
92
|
+
expect(addMethod?.kind).toBe('method');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('Go files', () => {
|
|
96
|
+
it('should scan Go file and extract elements', async () => {
|
|
97
|
+
const result = await scanFile(join(TESTDATA, 'sample.go'));
|
|
98
|
+
expect(result.errors).toHaveLength(0);
|
|
99
|
+
expect(result.language).toBe('go');
|
|
100
|
+
expect(result.elements.length).toBeGreaterThan(0);
|
|
101
|
+
});
|
|
102
|
+
it('should find exported Go functions', async () => {
|
|
103
|
+
const result = await scanFile(join(TESTDATA, 'sample.go'));
|
|
104
|
+
const names = result.elements.map(e => e.name);
|
|
105
|
+
expect(names).toContain('Greet');
|
|
106
|
+
expect(names).toContain('FetchData');
|
|
107
|
+
expect(names).toContain('NewCalculator');
|
|
108
|
+
expect(names).toContain('ProcessItems');
|
|
109
|
+
});
|
|
110
|
+
it('should find Go types', async () => {
|
|
111
|
+
const result = await scanFile(join(TESTDATA, 'sample.go'));
|
|
112
|
+
const names = result.elements.map(e => e.name);
|
|
113
|
+
expect(names).toContain('Config');
|
|
114
|
+
expect(names).toContain('Calculator');
|
|
115
|
+
expect(names).toContain('Client');
|
|
116
|
+
});
|
|
117
|
+
it('should find Go methods', async () => {
|
|
118
|
+
const result = await scanFile(join(TESTDATA, 'sample.go'));
|
|
119
|
+
const names = result.elements.map(e => e.name);
|
|
120
|
+
expect(names).toContain('Add');
|
|
121
|
+
expect(names).toContain('Multiply');
|
|
122
|
+
});
|
|
123
|
+
it('should exclude non-exported Go functions', async () => {
|
|
124
|
+
const result = await scanFile(join(TESTDATA, 'sample.go'));
|
|
125
|
+
const names = result.elements.map(e => e.name);
|
|
126
|
+
expect(names).not.toContain('privateFunction');
|
|
127
|
+
expect(names).not.toContain('privateMethod');
|
|
128
|
+
});
|
|
129
|
+
it('should extract Go doc comments', async () => {
|
|
130
|
+
const result = await scanFile(join(TESTDATA, 'sample.go'));
|
|
131
|
+
const greet = result.elements.find(e => e.name === 'Greet');
|
|
132
|
+
expect(greet?.docstring).toContain('generates a greeting');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('Rust files', () => {
|
|
136
|
+
it('should scan Rust file and extract elements', async () => {
|
|
137
|
+
const result = await scanFile(join(TESTDATA, 'sample.rs'));
|
|
138
|
+
expect(result.errors).toHaveLength(0);
|
|
139
|
+
expect(result.language).toBe('rust');
|
|
140
|
+
expect(result.elements.length).toBeGreaterThan(0);
|
|
141
|
+
});
|
|
142
|
+
it('should find pub Rust functions', async () => {
|
|
143
|
+
const result = await scanFile(join(TESTDATA, 'sample.rs'));
|
|
144
|
+
const names = result.elements.map(e => e.name);
|
|
145
|
+
expect(names).toContain('greet');
|
|
146
|
+
expect(names).toContain('fetch_data');
|
|
147
|
+
expect(names).toContain('process_items');
|
|
148
|
+
});
|
|
149
|
+
it('should find pub Rust types', async () => {
|
|
150
|
+
const result = await scanFile(join(TESTDATA, 'sample.rs'));
|
|
151
|
+
const names = result.elements.map(e => e.name);
|
|
152
|
+
expect(names).toContain('Config');
|
|
153
|
+
expect(names).toContain('Calculator');
|
|
154
|
+
expect(names).toContain('Client');
|
|
155
|
+
});
|
|
156
|
+
it('should find pub impl methods', async () => {
|
|
157
|
+
const result = await scanFile(join(TESTDATA, 'sample.rs'));
|
|
158
|
+
const names = result.elements.map(e => e.name);
|
|
159
|
+
expect(names).toContain('new');
|
|
160
|
+
expect(names).toContain('add');
|
|
161
|
+
expect(names).toContain('multiply');
|
|
162
|
+
});
|
|
163
|
+
it('should exclude non-pub Rust functions', async () => {
|
|
164
|
+
const result = await scanFile(join(TESTDATA, 'sample.rs'));
|
|
165
|
+
const names = result.elements.map(e => e.name);
|
|
166
|
+
expect(names).not.toContain('private_function');
|
|
167
|
+
expect(names).not.toContain('private_method');
|
|
168
|
+
});
|
|
169
|
+
it('should detect async Rust functions', async () => {
|
|
170
|
+
const result = await scanFile(join(TESTDATA, 'sample.rs'));
|
|
171
|
+
const fetchData = result.elements.find(e => e.name === 'fetch_data');
|
|
172
|
+
expect(fetchData?.isAsync).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
it('should extract Rust doc comments', async () => {
|
|
175
|
+
const result = await scanFile(join(TESTDATA, 'sample.rs'));
|
|
176
|
+
const greet = result.elements.find(e => e.name === 'greet');
|
|
177
|
+
expect(greet?.docstring).toContain('Generate a greeting');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('scanDirectory', () => {
|
|
182
|
+
it('should scan multiple files in directory', async () => {
|
|
183
|
+
const result = await scanDirectory(TESTDATA, {
|
|
184
|
+
include: ['**/*.py', '**/*.ts', '**/*.go', '**/*.rs'],
|
|
185
|
+
exclude: [],
|
|
186
|
+
});
|
|
187
|
+
expect(result.files.length).toBeGreaterThanOrEqual(4);
|
|
188
|
+
expect(result.totalElements).toBeGreaterThan(0);
|
|
189
|
+
expect(result.errors).toHaveLength(0);
|
|
190
|
+
});
|
|
191
|
+
it('should respect include patterns', async () => {
|
|
192
|
+
const result = await scanDirectory(TESTDATA, {
|
|
193
|
+
include: ['**/*.py'],
|
|
194
|
+
exclude: [],
|
|
195
|
+
});
|
|
196
|
+
expect(result.files.every(f => f.language === 'python')).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
it('should have files from all languages', async () => {
|
|
199
|
+
const result = await scanDirectory(TESTDATA, {
|
|
200
|
+
include: ['**/*.py', '**/*.ts', '**/*.go', '**/*.rs'],
|
|
201
|
+
exclude: [],
|
|
202
|
+
});
|
|
203
|
+
const languages = new Set(result.files.map(f => f.language));
|
|
204
|
+
expect(languages.has('python')).toBe(true);
|
|
205
|
+
expect(languages.has('typescript')).toBe(true);
|
|
206
|
+
expect(languages.has('go')).toBe(true);
|
|
207
|
+
expect(languages.has('rust')).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a parameter in a function/method signature
|
|
3
|
+
*/
|
|
4
|
+
export interface Parameter {
|
|
5
|
+
name: string;
|
|
6
|
+
type?: string;
|
|
7
|
+
default?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Represents an extracted API element (function, class, method)
|
|
12
|
+
*/
|
|
13
|
+
export interface APIElement {
|
|
14
|
+
kind: 'function' | 'class' | 'method';
|
|
15
|
+
name: string;
|
|
16
|
+
signature: string;
|
|
17
|
+
parameters: Parameter[];
|
|
18
|
+
returnType?: string;
|
|
19
|
+
docstring?: string;
|
|
20
|
+
filePath: string;
|
|
21
|
+
lineNumber: number;
|
|
22
|
+
parentClass?: string;
|
|
23
|
+
isAsync?: boolean;
|
|
24
|
+
isExported?: boolean;
|
|
25
|
+
isPublic?: boolean;
|
|
26
|
+
imports?: string[];
|
|
27
|
+
sourceContext?: string;
|
|
28
|
+
packageName?: string;
|
|
29
|
+
relatedTypes?: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Result of scanning a file
|
|
33
|
+
*/
|
|
34
|
+
export interface ScanResult {
|
|
35
|
+
filePath: string;
|
|
36
|
+
language: 'python' | 'javascript' | 'typescript' | 'go' | 'rust';
|
|
37
|
+
elements: APIElement[];
|
|
38
|
+
errors: string[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Scanner interface - each language implements this
|
|
42
|
+
*/
|
|
43
|
+
export interface Scanner {
|
|
44
|
+
/** Languages this scanner supports */
|
|
45
|
+
languages: string[];
|
|
46
|
+
/** Scan a single file and extract API elements */
|
|
47
|
+
scanFile(filePath: string): Promise<ScanResult>;
|
|
48
|
+
/** Check if this scanner can handle the given file */
|
|
49
|
+
canHandle(filePath: string): boolean;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Scanner, ScanResult } from './types.js';
|
|
2
|
+
export declare class TypeScriptScanner implements Scanner {
|
|
3
|
+
languages: string[];
|
|
4
|
+
canHandle(filePath: string): boolean;
|
|
5
|
+
scanFile(filePath: string): Promise<ScanResult>;
|
|
6
|
+
/**
|
|
7
|
+
* Extract all import statements from the file
|
|
8
|
+
*/
|
|
9
|
+
private extractImports;
|
|
10
|
+
/**
|
|
11
|
+
* Infer package name from file path
|
|
12
|
+
*/
|
|
13
|
+
private inferPackageName;
|
|
14
|
+
/**
|
|
15
|
+
* Get source context around a node (few lines before/after)
|
|
16
|
+
*/
|
|
17
|
+
private getSourceContext;
|
|
18
|
+
private visit;
|
|
19
|
+
private isPrivate;
|
|
20
|
+
private isPrivateMember;
|
|
21
|
+
private isExported;
|
|
22
|
+
private getPropertyName;
|
|
23
|
+
private extractFunction;
|
|
24
|
+
private extractArrowFunction;
|
|
25
|
+
private extractClass;
|
|
26
|
+
private extractMethod;
|
|
27
|
+
private extractConstructor;
|
|
28
|
+
private extractParameters;
|
|
29
|
+
private buildSignature;
|
|
30
|
+
private buildArrowSignature;
|
|
31
|
+
private buildMethodSignature;
|
|
32
|
+
private buildConstructorSignature;
|
|
33
|
+
private getJSDoc;
|
|
34
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
export class TypeScriptScanner {
|
|
4
|
+
languages = ['typescript', 'javascript'];
|
|
5
|
+
canHandle(filePath) {
|
|
6
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(filePath) && !filePath.includes('.d.ts');
|
|
7
|
+
}
|
|
8
|
+
async scanFile(filePath) {
|
|
9
|
+
const language = filePath.endsWith('.ts') || filePath.endsWith('.tsx')
|
|
10
|
+
? 'typescript'
|
|
11
|
+
: 'javascript';
|
|
12
|
+
try {
|
|
13
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
14
|
+
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, filePath.endsWith('.tsx') || filePath.endsWith('.jsx')
|
|
15
|
+
? ts.ScriptKind.TSX
|
|
16
|
+
: ts.ScriptKind.TS);
|
|
17
|
+
const elements = [];
|
|
18
|
+
const errors = [];
|
|
19
|
+
// Extract imports for context
|
|
20
|
+
const imports = this.extractImports(sourceFile);
|
|
21
|
+
const packageName = this.inferPackageName(filePath);
|
|
22
|
+
this.visit(sourceFile, sourceFile, elements, filePath, source, imports, packageName);
|
|
23
|
+
return {
|
|
24
|
+
filePath,
|
|
25
|
+
language,
|
|
26
|
+
elements,
|
|
27
|
+
errors
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
return {
|
|
32
|
+
filePath,
|
|
33
|
+
language,
|
|
34
|
+
elements: [],
|
|
35
|
+
errors: [`Failed to parse: ${err}`]
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Extract all import statements from the file
|
|
41
|
+
*/
|
|
42
|
+
extractImports(sourceFile) {
|
|
43
|
+
const imports = [];
|
|
44
|
+
ts.forEachChild(sourceFile, node => {
|
|
45
|
+
if (ts.isImportDeclaration(node)) {
|
|
46
|
+
imports.push(node.getText(sourceFile));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return imports;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Infer package name from file path
|
|
53
|
+
*/
|
|
54
|
+
inferPackageName(filePath) {
|
|
55
|
+
// Try to find package.json and extract name
|
|
56
|
+
const parts = filePath.split('/');
|
|
57
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
58
|
+
if (parts[i] === 'src' && i > 0) {
|
|
59
|
+
// Likely package name is parent of src
|
|
60
|
+
return parts[i - 1];
|
|
61
|
+
}
|
|
62
|
+
if (parts[i] === 'packages' && i < parts.length - 1) {
|
|
63
|
+
return parts[i + 1];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Fallback: use directory name
|
|
67
|
+
const srcIndex = filePath.lastIndexOf('/src/');
|
|
68
|
+
if (srcIndex > 0) {
|
|
69
|
+
const beforeSrc = filePath.slice(0, srcIndex);
|
|
70
|
+
return beforeSrc.split('/').pop() || 'unknown';
|
|
71
|
+
}
|
|
72
|
+
return 'unknown';
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get source context around a node (few lines before/after)
|
|
76
|
+
*/
|
|
77
|
+
getSourceContext(source, lineNumber, contextLines = 3) {
|
|
78
|
+
const lines = source.split('\n');
|
|
79
|
+
const start = Math.max(0, lineNumber - contextLines - 1);
|
|
80
|
+
const end = Math.min(lines.length, lineNumber + contextLines);
|
|
81
|
+
return lines.slice(start, end).join('\n');
|
|
82
|
+
}
|
|
83
|
+
visit(node, sourceFile, elements, filePath, source, imports, packageName, parentClass) {
|
|
84
|
+
// Function declarations
|
|
85
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
86
|
+
const name = node.name.text;
|
|
87
|
+
if (!this.isPrivate(name) && this.isExported(node)) {
|
|
88
|
+
const element = this.extractFunction(node, sourceFile, filePath);
|
|
89
|
+
element.imports = imports;
|
|
90
|
+
element.packageName = packageName;
|
|
91
|
+
element.sourceContext = this.getSourceContext(source, element.lineNumber, 5);
|
|
92
|
+
elements.push(element);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Arrow functions assigned to const (exported)
|
|
96
|
+
if (ts.isVariableStatement(node) && this.isExported(node)) {
|
|
97
|
+
for (const decl of node.declarationList.declarations) {
|
|
98
|
+
if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
99
|
+
if (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) {
|
|
100
|
+
const name = decl.name.text;
|
|
101
|
+
if (!this.isPrivate(name)) {
|
|
102
|
+
const element = this.extractArrowFunction(decl, decl.initializer, sourceFile, filePath);
|
|
103
|
+
element.imports = imports;
|
|
104
|
+
element.packageName = packageName;
|
|
105
|
+
element.sourceContext = this.getSourceContext(source, element.lineNumber, 5);
|
|
106
|
+
elements.push(element);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Class declarations
|
|
113
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
114
|
+
const name = node.name.text;
|
|
115
|
+
if (!this.isPrivate(name) && this.isExported(node)) {
|
|
116
|
+
const classElement = this.extractClass(node, sourceFile, filePath);
|
|
117
|
+
classElement.imports = imports;
|
|
118
|
+
classElement.packageName = packageName;
|
|
119
|
+
classElement.sourceContext = this.getSourceContext(source, classElement.lineNumber, 10);
|
|
120
|
+
elements.push(classElement);
|
|
121
|
+
// Extract methods
|
|
122
|
+
for (const member of node.members) {
|
|
123
|
+
if (ts.isMethodDeclaration(member) && member.name) {
|
|
124
|
+
const methodName = this.getPropertyName(member.name);
|
|
125
|
+
if (methodName && !this.isPrivateMember(member) && !methodName.startsWith('_')) {
|
|
126
|
+
const methodElement = this.extractMethod(member, sourceFile, filePath, name);
|
|
127
|
+
methodElement.imports = imports;
|
|
128
|
+
methodElement.packageName = packageName;
|
|
129
|
+
methodElement.sourceContext = this.getSourceContext(source, methodElement.lineNumber, 5);
|
|
130
|
+
elements.push(methodElement);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (ts.isConstructorDeclaration(member)) {
|
|
134
|
+
const ctorElement = this.extractConstructor(member, sourceFile, filePath, name);
|
|
135
|
+
ctorElement.imports = imports;
|
|
136
|
+
ctorElement.packageName = packageName;
|
|
137
|
+
ctorElement.sourceContext = this.getSourceContext(source, ctorElement.lineNumber, 5);
|
|
138
|
+
elements.push(ctorElement);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Recurse into children (but not into function/class bodies for top-level scan)
|
|
144
|
+
if (ts.isSourceFile(node) || ts.isModuleBlock(node)) {
|
|
145
|
+
ts.forEachChild(node, child => this.visit(child, sourceFile, elements, filePath, source, imports, packageName, parentClass));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
isPrivate(name) {
|
|
149
|
+
return name.startsWith('_') && !name.startsWith('__');
|
|
150
|
+
}
|
|
151
|
+
isPrivateMember(node) {
|
|
152
|
+
const modifiers = ts.getModifiers(node);
|
|
153
|
+
if (modifiers) {
|
|
154
|
+
for (const mod of modifiers) {
|
|
155
|
+
if (mod.kind === ts.SyntaxKind.PrivateKeyword)
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
isExported(node) {
|
|
162
|
+
const modifiers = ts.getModifiers(node);
|
|
163
|
+
if (modifiers) {
|
|
164
|
+
for (const mod of modifiers) {
|
|
165
|
+
if (mod.kind === ts.SyntaxKind.ExportKeyword)
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Also check for default export or if it's a top-level declaration in a module
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
getPropertyName(name) {
|
|
173
|
+
if (ts.isIdentifier(name))
|
|
174
|
+
return name.text;
|
|
175
|
+
if (ts.isStringLiteral(name))
|
|
176
|
+
return name.text;
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
extractFunction(node, sourceFile, filePath) {
|
|
180
|
+
const name = node.name?.text ?? 'anonymous';
|
|
181
|
+
const isAsync = node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
|
|
182
|
+
return {
|
|
183
|
+
kind: 'function',
|
|
184
|
+
name,
|
|
185
|
+
signature: this.buildSignature(node, sourceFile),
|
|
186
|
+
parameters: this.extractParameters(node.parameters, sourceFile),
|
|
187
|
+
returnType: node.type ? node.type.getText(sourceFile) : undefined,
|
|
188
|
+
docstring: this.getJSDoc(node, sourceFile),
|
|
189
|
+
filePath,
|
|
190
|
+
lineNumber: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
|
|
191
|
+
isAsync,
|
|
192
|
+
isExported: true,
|
|
193
|
+
isPublic: true
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
extractArrowFunction(decl, fn, sourceFile, filePath) {
|
|
197
|
+
const name = ts.isIdentifier(decl.name) ? decl.name.text : 'anonymous';
|
|
198
|
+
const isAsync = fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
|
|
199
|
+
return {
|
|
200
|
+
kind: 'function',
|
|
201
|
+
name,
|
|
202
|
+
signature: this.buildArrowSignature(decl, fn, sourceFile),
|
|
203
|
+
parameters: this.extractParameters(fn.parameters, sourceFile),
|
|
204
|
+
returnType: fn.type ? fn.type.getText(sourceFile) : undefined,
|
|
205
|
+
docstring: this.getJSDoc(decl.parent.parent, sourceFile),
|
|
206
|
+
filePath,
|
|
207
|
+
lineNumber: sourceFile.getLineAndCharacterOfPosition(decl.getStart()).line + 1,
|
|
208
|
+
isAsync,
|
|
209
|
+
isExported: true,
|
|
210
|
+
isPublic: true
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
extractClass(node, sourceFile, filePath) {
|
|
214
|
+
const name = node.name?.text ?? 'AnonymousClass';
|
|
215
|
+
let signature = `class ${name}`;
|
|
216
|
+
if (node.heritageClauses) {
|
|
217
|
+
for (const clause of node.heritageClauses) {
|
|
218
|
+
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
|
|
219
|
+
signature += ` extends ${clause.types.map(t => t.getText(sourceFile)).join(', ')}`;
|
|
220
|
+
}
|
|
221
|
+
if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
|
|
222
|
+
signature += ` implements ${clause.types.map(t => t.getText(sourceFile)).join(', ')}`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
kind: 'class',
|
|
228
|
+
name,
|
|
229
|
+
signature,
|
|
230
|
+
parameters: [],
|
|
231
|
+
docstring: this.getJSDoc(node, sourceFile),
|
|
232
|
+
filePath,
|
|
233
|
+
lineNumber: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
|
|
234
|
+
isExported: true,
|
|
235
|
+
isPublic: true
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
extractMethod(node, sourceFile, filePath, parentClass) {
|
|
239
|
+
const name = this.getPropertyName(node.name) ?? 'anonymous';
|
|
240
|
+
const isAsync = node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
|
|
241
|
+
return {
|
|
242
|
+
kind: 'method',
|
|
243
|
+
name,
|
|
244
|
+
signature: this.buildMethodSignature(node, sourceFile),
|
|
245
|
+
parameters: this.extractParameters(node.parameters, sourceFile),
|
|
246
|
+
returnType: node.type ? node.type.getText(sourceFile) : undefined,
|
|
247
|
+
docstring: this.getJSDoc(node, sourceFile),
|
|
248
|
+
filePath,
|
|
249
|
+
lineNumber: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
|
|
250
|
+
parentClass,
|
|
251
|
+
isAsync,
|
|
252
|
+
isPublic: true
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
extractConstructor(node, sourceFile, filePath, parentClass) {
|
|
256
|
+
return {
|
|
257
|
+
kind: 'method',
|
|
258
|
+
name: 'constructor',
|
|
259
|
+
signature: this.buildConstructorSignature(node, sourceFile),
|
|
260
|
+
parameters: this.extractParameters(node.parameters, sourceFile),
|
|
261
|
+
docstring: this.getJSDoc(node, sourceFile),
|
|
262
|
+
filePath,
|
|
263
|
+
lineNumber: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
|
|
264
|
+
parentClass,
|
|
265
|
+
isPublic: true
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
extractParameters(params, sourceFile) {
|
|
269
|
+
return params.map(p => {
|
|
270
|
+
const name = ts.isIdentifier(p.name) ? p.name.text : p.name.getText(sourceFile);
|
|
271
|
+
return {
|
|
272
|
+
name: p.dotDotDotToken ? `...${name}` : name,
|
|
273
|
+
type: p.type ? p.type.getText(sourceFile) : undefined,
|
|
274
|
+
default: p.initializer ? p.initializer.getText(sourceFile) : undefined
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
buildSignature(node, sourceFile) {
|
|
279
|
+
const async = node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ? 'async ' : '';
|
|
280
|
+
const name = node.name.text;
|
|
281
|
+
const typeParams = node.typeParameters
|
|
282
|
+
? `<${node.typeParameters.map(t => t.getText(sourceFile)).join(', ')}>`
|
|
283
|
+
: '';
|
|
284
|
+
const params = node.parameters.map(p => p.getText(sourceFile)).join(', ');
|
|
285
|
+
const returnType = node.type ? `: ${node.type.getText(sourceFile)}` : '';
|
|
286
|
+
return `${async}function ${name}${typeParams}(${params})${returnType}`;
|
|
287
|
+
}
|
|
288
|
+
buildArrowSignature(decl, fn, sourceFile) {
|
|
289
|
+
const name = decl.name.text;
|
|
290
|
+
const async = fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ? 'async ' : '';
|
|
291
|
+
const typeParams = fn.typeParameters
|
|
292
|
+
? `<${fn.typeParameters.map(t => t.getText(sourceFile)).join(', ')}>`
|
|
293
|
+
: '';
|
|
294
|
+
const params = fn.parameters.map(p => p.getText(sourceFile)).join(', ');
|
|
295
|
+
const returnType = fn.type ? `: ${fn.type.getText(sourceFile)}` : '';
|
|
296
|
+
return `const ${name} = ${async}${typeParams}(${params})${returnType} => ...`;
|
|
297
|
+
}
|
|
298
|
+
buildMethodSignature(node, sourceFile) {
|
|
299
|
+
const async = node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ? 'async ' : '';
|
|
300
|
+
const name = this.getPropertyName(node.name);
|
|
301
|
+
const typeParams = node.typeParameters
|
|
302
|
+
? `<${node.typeParameters.map(t => t.getText(sourceFile)).join(', ')}>`
|
|
303
|
+
: '';
|
|
304
|
+
const params = node.parameters.map(p => p.getText(sourceFile)).join(', ');
|
|
305
|
+
const returnType = node.type ? `: ${node.type.getText(sourceFile)}` : '';
|
|
306
|
+
return `${async}${name}${typeParams}(${params})${returnType}`;
|
|
307
|
+
}
|
|
308
|
+
buildConstructorSignature(node, sourceFile) {
|
|
309
|
+
const params = node.parameters.map(p => p.getText(sourceFile)).join(', ');
|
|
310
|
+
return `constructor(${params})`;
|
|
311
|
+
}
|
|
312
|
+
getJSDoc(node, sourceFile) {
|
|
313
|
+
// TypeScript's JSDoc is not in public types but exists at runtime
|
|
314
|
+
const jsDocNodes = node.jsDoc;
|
|
315
|
+
if (!jsDocNodes || jsDocNodes.length === 0)
|
|
316
|
+
return undefined;
|
|
317
|
+
const jsDoc = jsDocNodes[0];
|
|
318
|
+
if (jsDoc.comment) {
|
|
319
|
+
if (typeof jsDoc.comment === 'string') {
|
|
320
|
+
return jsDoc.comment;
|
|
321
|
+
}
|
|
322
|
+
// Handle JSDocComment[] for complex comments
|
|
323
|
+
return jsDoc.comment.map(c => c.getText(sourceFile)).join('');
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|