reverse-engine 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.

Potentially problematic release.


This version of reverse-engine might be problematic. Click here for more details.

package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # reverse-engine
2
+
3
+ 웹 서비스 역분석 자동화 도구 — 소스코드를 넣으면 **Excel 문서**, **Mermaid 다이어그램**, **테스트 코드**가 자동 생성됩니다.
4
+
5
+ ## 설치
6
+
7
+ ```bash
8
+ npm install -g reverse-engine
9
+ ```
10
+
11
+ ## 사용법
12
+
13
+ ### 전체 파이프라인 (한 줄)
14
+
15
+ ```bash
16
+ reverse-engine full --source ./my-project --output ./output
17
+ ```
18
+
19
+ 이 명령 하나로:
20
+ 1. 소스코드 분석 (컴포넌트, 함수, API, 라우트, 의존성)
21
+ 2. Excel 리포트 생성 (5개 시트)
22
+ 3. Mermaid 컴포넌트 다이어그램 생성
23
+ 4. E2E + API 테스트 코드 자동 생성
24
+
25
+ ### 개별 명령어
26
+
27
+ ```bash
28
+ # 소스코드 분석
29
+ reverse-engine analyze ./my-project
30
+
31
+ # 리포트 생성
32
+ reverse-engine report --input output/analysis.json --format excel,mermaid
33
+
34
+ # 테스트 코드 생성
35
+ reverse-engine test --input output/analysis.json --type e2e,api
36
+ ```
37
+
38
+ ## 지원 프레임워크
39
+
40
+ - **React** / Next.js
41
+ - **Vue** / Nuxt
42
+ - **Angular**
43
+ - **Svelte**
44
+ - TypeScript / JavaScript
45
+
46
+ ## 출력물
47
+
48
+ ### Excel 리포트 (5개 시트)
49
+
50
+ | 시트 | 내용 |
51
+ |------|------|
52
+ | 컴포넌트 목록 | 이름, 파일, 타입, Props, 하위 컴포넌트, Hooks |
53
+ | API 엔드포인트 | Method, URL, 파일, 함수명, 라인 |
54
+ | 라우트 | Path, Component, 파일 |
55
+ | 함수 호출 체인 | 함수명, 호출/피호출 관계, async/export 여부 |
56
+ | 의존성 패키지 | 패키지명, 버전, prod/dev 구분 |
57
+
58
+ ### Mermaid 다이어그램
59
+
60
+ 컴포넌트 관계 + 라우트 매핑 플로우차트 자동 생성
61
+
62
+ ### 테스트 코드
63
+
64
+ - **E2E 테스트**: 라우트별 페이지 로드 + 컴포넌트 렌더링
65
+ - **API 테스트**: 엔드포인트별 상태코드 검증
66
+
67
+ ## 프로그래매틱 사용
68
+
69
+ ```typescript
70
+ import { analyze, generateReport, generateTests } from 'reverse-engine';
71
+
72
+ const result = await analyze('./my-project');
73
+ await generateReport(result, { formats: ['excel', 'mermaid'] });
74
+ await generateTests(result, { types: ['e2e', 'api'] });
75
+ ```
76
+
77
+ ## 분석 결과 예시
78
+
79
+ ```
80
+ 프레임워크: React
81
+ 컴포넌트: 7개 (App, Dashboard, Settings, Login, Layout, StatsCard, ActivityFeed)
82
+ 함수: 16개 (loadData, handleRefresh, handleExport, handleLogin, handleSave...)
83
+ API: 7개 (GET /api/dashboard/stats, POST /api/auth/login, PUT /api/settings/profile...)
84
+ 라우트: 3개 (/, /settings, /login)
85
+ 의존성: 8개 (react, axios, zustand...)
86
+ ```
87
+
88
+ ## License
89
+
90
+ MIT
@@ -0,0 +1,2 @@
1
+ import type { ApiClientCall } from '../../types.js';
2
+ export declare function extractApiCalls(source: string, filePath: string, ext: string): ApiClientCall[];
@@ -0,0 +1,86 @@
1
+ import _traverse from '@babel/traverse';
2
+ import { parseSource, parseVue } from '../parsers/parse.js';
3
+ const traverse = _traverse.default || _traverse;
4
+ export function extractApiCalls(source, filePath, ext) {
5
+ const ast = ext === '.vue' ? parseVue(source) : parseSource(source, ext);
6
+ const calls = [];
7
+ traverse(ast, {
8
+ CallExpression(path) {
9
+ const callee = path.node.callee;
10
+ let funcText = '';
11
+ let method = '';
12
+ // fetch('url')
13
+ if (callee?.type === 'Identifier' && callee.name === 'fetch') {
14
+ funcText = 'fetch';
15
+ method = 'GET';
16
+ // 두 번째 인자에서 method 확인
17
+ const opts = path.node.arguments[1];
18
+ if (opts?.type === 'ObjectExpression') {
19
+ for (const prop of opts.properties) {
20
+ if (prop.key?.name === 'method' && prop.value?.type === 'StringLiteral') {
21
+ method = prop.value.value.toUpperCase();
22
+ }
23
+ }
24
+ }
25
+ }
26
+ // axios.get/post/put/delete, api.get, $http.get, requests.get
27
+ if (callee?.type === 'MemberExpression') {
28
+ const obj = callee.object?.name || '';
29
+ const prop = callee.property?.name || '';
30
+ if (['axios', 'api', '$http', 'http', 'requests', 'this'].includes(obj) ||
31
+ obj.endsWith('Client') || obj.endsWith('Api')) {
32
+ const methodMap = {
33
+ get: 'GET', post: 'POST', put: 'PUT', delete: 'DELETE',
34
+ patch: 'PATCH', head: 'HEAD', options: 'OPTIONS',
35
+ };
36
+ if (methodMap[prop]) {
37
+ funcText = `${obj}.${prop}`;
38
+ method = methodMap[prop];
39
+ }
40
+ }
41
+ }
42
+ if (!funcText || !method)
43
+ return;
44
+ // URL 추출
45
+ const firstArg = path.node.arguments[0];
46
+ let url = '';
47
+ if (firstArg?.type === 'StringLiteral') {
48
+ url = firstArg.value;
49
+ }
50
+ else if (firstArg?.type === 'TemplateLiteral' && firstArg.quasis?.[0]) {
51
+ url = firstArg.quasis.map((q) => q.value.raw).join('*');
52
+ }
53
+ if (!url)
54
+ return;
55
+ // 상위 함수명 찾기
56
+ const functionName = findEnclosingFunctionName(path);
57
+ calls.push({
58
+ method,
59
+ urlPattern: url,
60
+ filePath,
61
+ line: path.node.loc?.start.line || 0,
62
+ functionName,
63
+ });
64
+ },
65
+ });
66
+ return calls;
67
+ }
68
+ function findEnclosingFunctionName(path) {
69
+ let current = path.parentPath;
70
+ while (current) {
71
+ if (current.node.type === 'FunctionDeclaration' && current.node.id) {
72
+ return current.node.id.name;
73
+ }
74
+ if (current.node.type === 'VariableDeclarator' && current.node.id?.name) {
75
+ const init = current.node.init;
76
+ if (init?.type === 'ArrowFunctionExpression' || init?.type === 'FunctionExpression') {
77
+ return current.node.id.name;
78
+ }
79
+ }
80
+ if (current.node.type === 'ClassMethod' && current.node.key?.name) {
81
+ return current.node.key.name;
82
+ }
83
+ current = current.parentPath;
84
+ }
85
+ return '<anonymous>';
86
+ }
@@ -0,0 +1,2 @@
1
+ import type { ComponentInfo } from '../../types.js';
2
+ export declare function extractComponents(source: string, filePath: string, ext: string): ComponentInfo[];
@@ -0,0 +1,189 @@
1
+ import _traverse from '@babel/traverse';
2
+ import { parseSource, parseVue } from '../parsers/parse.js';
3
+ const traverse = _traverse.default || _traverse;
4
+ export function extractComponents(source, filePath, ext) {
5
+ const ast = ext === '.vue' ? parseVue(source) : parseSource(source, ext);
6
+ const components = [];
7
+ if (ext === '.vue') {
8
+ // Vue SFC = 파일 자체가 컴포넌트
9
+ const name = filePath.split('/').pop()?.replace('.vue', '') || filePath;
10
+ const hooks = collectHooks(ast);
11
+ const apiCalls = collectApiPatterns(ast);
12
+ components.push({
13
+ name,
14
+ filePath,
15
+ lineStart: 1,
16
+ lineEnd: source.split('\n').length,
17
+ componentType: inferType(filePath),
18
+ props: [],
19
+ children: [],
20
+ usedBy: [],
21
+ hooks,
22
+ apiCalls,
23
+ });
24
+ return components;
25
+ }
26
+ // React 컴포넌트 추출 (대문자 시작 + JSX 반환)
27
+ traverse(ast, {
28
+ FunctionDeclaration(path) {
29
+ const comp = tryExtractReactComponent(path, filePath, source);
30
+ if (comp)
31
+ components.push(comp);
32
+ },
33
+ VariableDeclarator(path) {
34
+ const init = path.node.init;
35
+ if (!init || (init.type !== 'ArrowFunctionExpression' && init.type !== 'FunctionExpression'))
36
+ return;
37
+ if (path.node.id?.type !== 'Identifier')
38
+ return;
39
+ const name = path.node.id.name;
40
+ if (!name[0] || name[0] !== name[0].toUpperCase())
41
+ return;
42
+ if (!hasJSX(path))
43
+ return;
44
+ const children = collectJSXChildren(path);
45
+ const hooks = collectHooks(path.get('init'));
46
+ const apiCalls = collectApiPatterns(path.get('init'));
47
+ components.push({
48
+ name,
49
+ filePath,
50
+ lineStart: path.node.loc?.start.line || 0,
51
+ lineEnd: path.node.loc?.end.line || 0,
52
+ componentType: inferType(filePath),
53
+ props: extractProps(init),
54
+ children,
55
+ usedBy: [],
56
+ hooks,
57
+ apiCalls,
58
+ });
59
+ },
60
+ });
61
+ return components;
62
+ }
63
+ function tryExtractReactComponent(path, filePath, source) {
64
+ const name = path.node.id?.name;
65
+ if (!name || name[0] !== name[0].toUpperCase())
66
+ return null;
67
+ if (!hasJSX(path))
68
+ return null;
69
+ return {
70
+ name,
71
+ filePath,
72
+ lineStart: path.node.loc?.start.line || 0,
73
+ lineEnd: path.node.loc?.end.line || 0,
74
+ componentType: inferType(filePath),
75
+ props: extractProps(path.node),
76
+ children: collectJSXChildren(path),
77
+ usedBy: [],
78
+ hooks: collectHooks(path),
79
+ apiCalls: collectApiPatterns(path),
80
+ };
81
+ }
82
+ function hasJSX(path) {
83
+ let found = false;
84
+ path.traverse({
85
+ JSXElement() { found = true; },
86
+ JSXFragment() { found = true; },
87
+ });
88
+ return found;
89
+ }
90
+ function collectJSXChildren(path) {
91
+ const children = new Set();
92
+ path.traverse({
93
+ JSXOpeningElement(jsxPath) {
94
+ const name = jsxPath.node.name;
95
+ let tagName = '';
96
+ if (name.type === 'JSXIdentifier')
97
+ tagName = name.name;
98
+ else if (name.type === 'JSXMemberExpression')
99
+ tagName = name.property?.name || '';
100
+ if (tagName && tagName[0] === tagName[0].toUpperCase()) {
101
+ children.add(tagName);
102
+ }
103
+ },
104
+ });
105
+ return [...children].sort();
106
+ }
107
+ function collectHooks(pathOrAst) {
108
+ const hooks = new Set();
109
+ const visitor = {
110
+ CallExpression(callPath) {
111
+ const name = callPath.node.callee?.name;
112
+ if (name?.startsWith('use'))
113
+ hooks.add(name);
114
+ },
115
+ };
116
+ if (pathOrAst.traverse) {
117
+ pathOrAst.traverse(visitor);
118
+ }
119
+ else {
120
+ traverse(pathOrAst, visitor);
121
+ }
122
+ return [...hooks].sort();
123
+ }
124
+ function collectApiPatterns(pathOrAst) {
125
+ const apis = new Set();
126
+ const visitor = {
127
+ CallExpression(callPath) {
128
+ const callee = callPath.node.callee;
129
+ let funcText = '';
130
+ if (callee?.type === 'MemberExpression') {
131
+ const obj = callee.object?.name || '';
132
+ const prop = callee.property?.name || '';
133
+ funcText = `${obj}.${prop}`;
134
+ }
135
+ else if (callee?.type === 'Identifier') {
136
+ funcText = callee.name;
137
+ }
138
+ if (funcText === 'fetch' || funcText.startsWith('axios.') || funcText.startsWith('api.') || funcText.startsWith('$http.')) {
139
+ const args = callPath.node.arguments;
140
+ const method = funcText.includes('.get') ? 'GET'
141
+ : funcText.includes('.post') ? 'POST'
142
+ : funcText.includes('.put') ? 'PUT'
143
+ : funcText.includes('.delete') ? 'DELETE'
144
+ : funcText === 'fetch' ? 'GET' : 'GET';
145
+ let url = '';
146
+ if (args[0]?.type === 'StringLiteral')
147
+ url = args[0].value;
148
+ else if (args[0]?.type === 'TemplateLiteral' && args[0].quasis?.[0]) {
149
+ url = args[0].quasis[0].value.raw;
150
+ }
151
+ if (url)
152
+ apis.add(`${method} ${url}`);
153
+ }
154
+ },
155
+ };
156
+ if (pathOrAst.traverse) {
157
+ pathOrAst.traverse(visitor);
158
+ }
159
+ else {
160
+ traverse(pathOrAst, visitor);
161
+ }
162
+ return [...apis].sort();
163
+ }
164
+ function extractProps(node) {
165
+ const params = node.params || [];
166
+ if (params.length === 0)
167
+ return [];
168
+ const first = params[0];
169
+ if (first.type === 'ObjectPattern') {
170
+ return first.properties
171
+ .filter((p) => p.type === 'ObjectProperty' || p.type === 'Property')
172
+ .map((p) => ({
173
+ name: p.key?.name || '?',
174
+ type: 'unknown',
175
+ required: true,
176
+ }));
177
+ }
178
+ return [];
179
+ }
180
+ function inferType(filePath) {
181
+ const p = filePath.toLowerCase();
182
+ if (p.includes('page') || p.includes('views') || p.includes('routes'))
183
+ return 'Page';
184
+ if (p.includes('layout'))
185
+ return 'Layout';
186
+ if (p.includes('util') || p.includes('helper') || p.includes('hoc'))
187
+ return 'Utility';
188
+ return 'Widget';
189
+ }
@@ -0,0 +1,2 @@
1
+ import type { FunctionInfo } from '../../types.js';
2
+ export declare function extractFunctions(source: string, filePath: string, ext: string): FunctionInfo[];
@@ -0,0 +1,111 @@
1
+ import _traverse from '@babel/traverse';
2
+ import { parseSource, parseVue } from '../parsers/parse.js';
3
+ // ESM 호환
4
+ const traverse = _traverse.default || _traverse;
5
+ export function extractFunctions(source, filePath, ext) {
6
+ const ast = ext === '.vue' ? parseVue(source) : parseSource(source, ext);
7
+ const functions = [];
8
+ traverse(ast, {
9
+ // function foo() {} / async function foo() {}
10
+ FunctionDeclaration(path) {
11
+ if (!path.node.id)
12
+ return;
13
+ functions.push(buildFunctionInfo(path, filePath));
14
+ },
15
+ // const foo = () => {} / const foo = function() {}
16
+ VariableDeclarator(path) {
17
+ const init = path.node.init;
18
+ if (!init)
19
+ return;
20
+ if (init.type !== 'ArrowFunctionExpression' && init.type !== 'FunctionExpression')
21
+ return;
22
+ if (path.node.id?.type !== 'Identifier')
23
+ return;
24
+ const name = path.node.id.name;
25
+ const calls = collectCallExpressions(path);
26
+ const isExported = path.parentPath?.parentPath?.node.type === 'ExportNamedDeclaration'
27
+ || path.parentPath?.parentPath?.node.type === 'ExportDefaultDeclaration';
28
+ functions.push({
29
+ name,
30
+ filePath,
31
+ lineStart: path.node.loc?.start.line || 0,
32
+ lineEnd: path.node.loc?.end.line || 0,
33
+ params: extractParams(init),
34
+ returnType: init.returnType?.typeAnnotation?.type,
35
+ calls,
36
+ calledBy: [],
37
+ isAsync: init.async || false,
38
+ isExported,
39
+ });
40
+ },
41
+ // class methods
42
+ ClassMethod(path) {
43
+ const name = path.node.key?.name;
44
+ if (!name)
45
+ return;
46
+ functions.push({
47
+ name,
48
+ filePath,
49
+ lineStart: path.node.loc?.start.line || 0,
50
+ lineEnd: path.node.loc?.end.line || 0,
51
+ params: extractParams(path.node),
52
+ returnType: undefined,
53
+ calls: collectCallExpressions(path),
54
+ calledBy: [],
55
+ isAsync: path.node.async || false,
56
+ isExported: false,
57
+ });
58
+ },
59
+ });
60
+ return functions;
61
+ }
62
+ function buildFunctionInfo(path, filePath) {
63
+ const node = path.node;
64
+ const isExported = path.parentPath?.node.type === 'ExportNamedDeclaration'
65
+ || path.parentPath?.node.type === 'ExportDefaultDeclaration';
66
+ return {
67
+ name: node.id.name,
68
+ filePath,
69
+ lineStart: node.loc?.start.line || 0,
70
+ lineEnd: node.loc?.end.line || 0,
71
+ params: extractParams(node),
72
+ returnType: node.returnType?.typeAnnotation?.type,
73
+ calls: collectCallExpressions(path),
74
+ calledBy: [],
75
+ isAsync: node.async || false,
76
+ isExported,
77
+ };
78
+ }
79
+ function extractParams(node) {
80
+ return (node.params || []).map((p) => {
81
+ if (p.type === 'Identifier')
82
+ return p.name;
83
+ if (p.type === 'AssignmentPattern' && p.left?.name)
84
+ return p.left.name;
85
+ if (p.type === 'ObjectPattern')
86
+ return '{...}';
87
+ if (p.type === 'RestElement')
88
+ return `...${p.argument?.name || ''}`;
89
+ return '?';
90
+ });
91
+ }
92
+ function collectCallExpressions(path) {
93
+ const calls = new Set();
94
+ path.traverse({
95
+ CallExpression(callPath) {
96
+ const callee = callPath.node.callee;
97
+ let name = '';
98
+ if (callee.type === 'Identifier') {
99
+ name = callee.name;
100
+ }
101
+ else if (callee.type === 'MemberExpression') {
102
+ // a.b.c → c
103
+ name = callee.property?.name || '';
104
+ }
105
+ if (name && name !== 'require') {
106
+ calls.add(name);
107
+ }
108
+ },
109
+ });
110
+ return [...calls].sort();
111
+ }
@@ -0,0 +1,2 @@
1
+ import type { RouteInfo } from '../../types.js';
2
+ export declare function extractRoutes(source: string, filePath: string, ext: string): RouteInfo[];
@@ -0,0 +1,98 @@
1
+ import _traverse from '@babel/traverse';
2
+ import { parseSource } from '../parsers/parse.js';
3
+ const traverse = _traverse.default || _traverse;
4
+ export function extractRoutes(source, filePath, ext) {
5
+ // Next.js/Nuxt 파일 기반 라우팅
6
+ if (filePath.includes('pages/') || filePath.includes('app/')) {
7
+ const route = inferFileBasedRoute(filePath);
8
+ if (route)
9
+ return [route];
10
+ }
11
+ if (ext === '.vue')
12
+ return []; // Vue Router는 별도 설정 파일에서
13
+ const ast = parseSource(source, ext);
14
+ const routes = [];
15
+ traverse(ast, {
16
+ // <Route path="..." element={<Comp />} />
17
+ JSXOpeningElement(path) {
18
+ const name = path.node.name;
19
+ if (name?.type !== 'JSXIdentifier' || name.name !== 'Route')
20
+ return;
21
+ let routePath = '';
22
+ let component = '';
23
+ for (const attr of path.node.attributes || []) {
24
+ if (attr.type !== 'JSXAttribute')
25
+ continue;
26
+ const attrName = attr.name?.name;
27
+ if (attrName === 'path' && attr.value?.type === 'StringLiteral') {
28
+ routePath = attr.value.value;
29
+ }
30
+ if ((attrName === 'element' || attrName === 'component') && attr.value?.type === 'JSXExpressionContainer') {
31
+ const expr = attr.value.expression;
32
+ if (expr.type === 'JSXElement') {
33
+ component = expr.openingElement?.name?.name || '';
34
+ }
35
+ else if (expr.type === 'Identifier') {
36
+ component = expr.name;
37
+ }
38
+ }
39
+ }
40
+ if (routePath) {
41
+ routes.push({
42
+ path: routePath,
43
+ component: component || '?',
44
+ filePath,
45
+ guards: [],
46
+ });
47
+ }
48
+ },
49
+ // Vue Router / generic: { path: '...', component: ... }
50
+ ObjectExpression(path) {
51
+ let routePath = '';
52
+ let component = '';
53
+ for (const prop of path.node.properties || []) {
54
+ if (prop.type !== 'ObjectProperty')
55
+ continue;
56
+ const key = prop.key?.name || prop.key?.value;
57
+ if (key === 'path' && prop.value?.type === 'StringLiteral') {
58
+ routePath = prop.value.value;
59
+ }
60
+ if (key === 'component' && prop.value?.type === 'Identifier') {
61
+ component = prop.value.name;
62
+ }
63
+ }
64
+ if (routePath.startsWith('/') && component) {
65
+ // 중복 방지
66
+ if (!routes.some(r => r.path === routePath)) {
67
+ routes.push({ path: routePath, component, filePath, guards: [] });
68
+ }
69
+ }
70
+ },
71
+ });
72
+ return routes;
73
+ }
74
+ function inferFileBasedRoute(filePath) {
75
+ const normalized = filePath.replace(/\\/g, '/');
76
+ let routePart = '';
77
+ if (normalized.includes('pages/')) {
78
+ routePart = normalized.split('pages/')[1];
79
+ }
80
+ else if (normalized.includes('app/')) {
81
+ routePart = normalized.split('app/')[1];
82
+ }
83
+ else {
84
+ return null;
85
+ }
86
+ const routePath = routePart
87
+ .replace(/\.(tsx?|jsx?|vue)$/, '')
88
+ .replace(/\/index$/, '')
89
+ .replace(/\/page$/, '')
90
+ .replace(/\[(\w+)\]/g, ':$1');
91
+ const component = filePath.split('/').pop()?.split('.')[0] || '';
92
+ return {
93
+ path: routePath ? `/${routePath}` : '/',
94
+ component,
95
+ filePath,
96
+ guards: [],
97
+ };
98
+ }
@@ -0,0 +1 @@
1
+ export declare function detectFramework(sourcePath: string): Promise<string>;
@@ -0,0 +1,22 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { join } from 'path';
3
+ export async function detectFramework(sourcePath) {
4
+ try {
5
+ const pkg = JSON.parse(await readFile(join(sourcePath, 'package.json'), 'utf-8'));
6
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
7
+ if (allDeps['next'])
8
+ return 'Next.js';
9
+ if (allDeps['nuxt'])
10
+ return 'Nuxt';
11
+ if (allDeps['@angular/core'])
12
+ return 'Angular';
13
+ if (allDeps['svelte'])
14
+ return 'Svelte';
15
+ if (allDeps['vue'])
16
+ return 'Vue';
17
+ if (allDeps['react'])
18
+ return 'React';
19
+ }
20
+ catch { /* no package.json */ }
21
+ return 'Unknown';
22
+ }
@@ -0,0 +1,7 @@
1
+ import type { AnalysisResult } from '../types.js';
2
+ export interface AnalyzeOptions {
3
+ include?: string[];
4
+ exclude?: string[];
5
+ framework?: string;
6
+ }
7
+ export declare function analyze(sourcePath: string, options?: AnalyzeOptions): Promise<AnalysisResult>;
@@ -0,0 +1,95 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { join, extname } from 'path';
3
+ import { glob } from 'glob';
4
+ import { extractComponents } from './extractors/component.js';
5
+ import { extractFunctions } from './extractors/function.js';
6
+ import { extractApiCalls } from './extractors/api-call.js';
7
+ import { extractRoutes } from './extractors/route.js';
8
+ import { detectFramework } from './framework.js';
9
+ export async function analyze(sourcePath, options = {}) {
10
+ const framework = options.framework && options.framework !== 'auto'
11
+ ? options.framework
12
+ : await detectFramework(sourcePath);
13
+ const include = options.include || ['**/*.{ts,tsx,js,jsx,mjs,vue}'];
14
+ const exclude = options.exclude || ['node_modules/**', 'dist/**', 'build/**', '.next/**', '**/*.test.*', '**/*.spec.*'];
15
+ // 파일 수집
16
+ const files = await glob(include, {
17
+ cwd: sourcePath,
18
+ ignore: exclude,
19
+ absolute: false,
20
+ });
21
+ let components = [];
22
+ let functions = [];
23
+ let apiClients = [];
24
+ let routes = [];
25
+ // 각 파일 분석
26
+ for (const file of files) {
27
+ const fullPath = join(sourcePath, file);
28
+ const source = await readFile(fullPath, 'utf-8');
29
+ const ext = extname(file);
30
+ try {
31
+ const fileComponents = extractComponents(source, file, ext);
32
+ const fileFunctions = extractFunctions(source, file, ext);
33
+ const fileApiCalls = extractApiCalls(source, file, ext);
34
+ const fileRoutes = extractRoutes(source, file, ext);
35
+ components.push(...fileComponents);
36
+ functions.push(...fileFunctions);
37
+ apiClients.push(...fileApiCalls);
38
+ routes.push(...fileRoutes);
39
+ }
40
+ catch {
41
+ // 파싱 실패한 파일은 건너뜀
42
+ }
43
+ }
44
+ // 역참조 구축
45
+ buildReverseReferences(functions, components);
46
+ // 의존성 추출
47
+ const dependencies = await extractDependencies(sourcePath);
48
+ return {
49
+ sourcePath,
50
+ framework,
51
+ components,
52
+ routes,
53
+ functions,
54
+ apiClients,
55
+ dependencies,
56
+ };
57
+ }
58
+ function buildReverseReferences(functions, components) {
59
+ // calledBy
60
+ for (const func of functions) {
61
+ for (const callee of func.calls) {
62
+ const target = functions.find(f => f.name === callee);
63
+ if (target && !target.calledBy.includes(func.name)) {
64
+ target.calledBy.push(func.name);
65
+ }
66
+ }
67
+ }
68
+ // usedBy
69
+ for (const comp of components) {
70
+ for (const child of comp.children) {
71
+ const target = components.find(c => c.name === child);
72
+ if (target && !target.usedBy.includes(comp.name)) {
73
+ target.usedBy.push(comp.name);
74
+ }
75
+ }
76
+ }
77
+ }
78
+ async function extractDependencies(sourcePath) {
79
+ const deps = [];
80
+ const pkgPath = join(sourcePath, 'package.json');
81
+ try {
82
+ const content = JSON.parse(await readFile(pkgPath, 'utf-8'));
83
+ for (const [name, version] of Object.entries(content.dependencies || {})) {
84
+ deps.push({ name, currentVersion: version, depType: 'Production' });
85
+ }
86
+ for (const [name, version] of Object.entries(content.devDependencies || {})) {
87
+ deps.push({ name, currentVersion: version, depType: 'Development' });
88
+ }
89
+ for (const [name, version] of Object.entries(content.peerDependencies || {})) {
90
+ deps.push({ name, currentVersion: version, depType: 'Peer' });
91
+ }
92
+ }
93
+ catch { /* no package.json */ }
94
+ return deps;
95
+ }
@@ -0,0 +1,5 @@
1
+ import type { File } from '@babel/types';
2
+ /** 소스코드를 AST로 파싱 (TS/TSX/JS/JSX 지원) */
3
+ export declare function parseSource(source: string, ext: string): File;
4
+ /** Vue SFC에서 <script> 블록 추출 후 파싱 */
5
+ export declare function parseVue(source: string): File;
@@ -0,0 +1,26 @@
1
+ import { parse as babelParse } from '@babel/parser';
2
+ /** 소스코드를 AST로 파싱 (TS/TSX/JS/JSX 지원) */
3
+ export function parseSource(source, ext) {
4
+ const isTS = ext === '.ts' || ext === '.tsx';
5
+ const isJSX = ext === '.tsx' || ext === '.jsx';
6
+ return babelParse(source, {
7
+ sourceType: 'module',
8
+ plugins: [
9
+ ...(isTS ? ['typescript'] : []),
10
+ ...(isJSX || isTS ? ['jsx'] : []),
11
+ 'decorators-legacy',
12
+ 'classProperties',
13
+ 'optionalChaining',
14
+ 'nullishCoalescingOperator',
15
+ 'dynamicImport',
16
+ ],
17
+ errorRecovery: true,
18
+ });
19
+ }
20
+ /** Vue SFC에서 <script> 블록 추출 후 파싱 */
21
+ export function parseVue(source) {
22
+ const scriptMatch = source.match(/<script[^>]*>([\s\S]*?)<\/script>/);
23
+ const scriptContent = scriptMatch?.[1] || '';
24
+ const isTS = source.includes('lang="ts"') || source.includes("lang='ts'");
25
+ return parseSource(scriptContent, isTS ? '.ts' : '.js');
26
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import { readFile, writeFile, mkdir } from 'fs/promises';
5
+ import { analyze } from '../analyzer/index.js';
6
+ import { generateReport } from '../docgen/index.js';
7
+ import { generateTests } from '../testgen/index.js';
8
+ const program = new Command();
9
+ program
10
+ .name('reverse-engine')
11
+ .version('0.1.0')
12
+ .description('웹 서비스 역분석 자동화 도구 - 소스코드 분석, 문서 생성, 테스트 자동화');
13
+ // analyze
14
+ program
15
+ .command('analyze <path>')
16
+ .description('소스코드 정적 분석')
17
+ .option('--framework <name>', '프레임워크 지정 (auto, react, vue, angular, next)', 'auto')
18
+ .option('--include <patterns>', '포함 패턴 (쉼표 구분)', 'src/**/*.{ts,tsx,js,jsx,vue}')
19
+ .option('--output <dir>', '출력 디렉토리', './output')
20
+ .action(async (sourcePath, opts) => {
21
+ console.log(chalk.green('▶'), '코드 분석 시작:', chalk.cyan(sourcePath));
22
+ const result = await analyze(sourcePath, {
23
+ framework: opts.framework,
24
+ include: opts.include.split(','),
25
+ });
26
+ console.log(chalk.green('✓'), '코드 분석 완료!');
27
+ console.log(` 프레임워크: ${result.framework}`);
28
+ console.log(` 컴포넌트: ${result.components.length}개`);
29
+ console.log(` 함수: ${result.functions.length}개`);
30
+ console.log(` API: ${result.apiClients.length}개`);
31
+ console.log(` 라우트: ${result.routes.length}개`);
32
+ console.log(` 의존성: ${result.dependencies.length}개`);
33
+ await mkdir(opts.output, { recursive: true });
34
+ const outputPath = `${opts.output}/analysis.json`;
35
+ await writeFile(outputPath, JSON.stringify(result, null, 2));
36
+ console.log(` 결과 저장: ${chalk.cyan(outputPath)}`);
37
+ });
38
+ // report
39
+ program
40
+ .command('report')
41
+ .description('분석 결과로 리포트 생성')
42
+ .requiredOption('--input <file>', '분석 결과 JSON 파일')
43
+ .option('--format <formats>', '출력 형식 (excel,mermaid)', 'excel,mermaid')
44
+ .option('--output <dir>', '출력 디렉토리', './output/reports')
45
+ .action(async (opts) => {
46
+ console.log(chalk.green('▶'), '리포트 생성 시작');
47
+ const data = JSON.parse(await readFile(opts.input, 'utf-8'));
48
+ const formats = opts.format.split(',');
49
+ const outputs = await generateReport(data, { formats, outputDir: opts.output });
50
+ console.log(chalk.green('✓'), '리포트 생성 완료!');
51
+ outputs.forEach(p => console.log(` → ${chalk.cyan(p)}`));
52
+ });
53
+ // test
54
+ program
55
+ .command('test')
56
+ .description('테스트 코드 자동 생성')
57
+ .requiredOption('--input <file>', '분석 결과 JSON 파일')
58
+ .option('--type <types>', '테스트 종류 (e2e,api)', 'e2e,api')
59
+ .option('--output <dir>', '출력 디렉토리', './output/tests')
60
+ .action(async (opts) => {
61
+ console.log(chalk.green('▶'), '테스트 코드 생성 시작');
62
+ const data = JSON.parse(await readFile(opts.input, 'utf-8'));
63
+ const types = opts.type.split(',');
64
+ const files = await generateTests(data, { types, outputDir: opts.output });
65
+ console.log(chalk.green('✓'), `테스트 코드 생성 완료! (${files.length}개 파일)`);
66
+ files.forEach(p => console.log(` → ${chalk.cyan(p)}`));
67
+ });
68
+ // full
69
+ program
70
+ .command('full')
71
+ .description('전체 파이프라인 (analyze → report → test)')
72
+ .requiredOption('--source <path>', '소스코드 경로')
73
+ .option('--output <dir>', '출력 디렉토리', './output')
74
+ .option('--framework <name>', '프레임워크', 'auto')
75
+ .action(async (opts) => {
76
+ console.log(chalk.green('\n◆'), 'ReversEngine 전체 파이프라인 시작\n');
77
+ // Step 1: 분석
78
+ console.log(chalk.gray('━'.repeat(50)));
79
+ const result = await analyze(opts.source, { framework: opts.framework });
80
+ console.log(chalk.green('✓'), `분석 완료: 컴포넌트 ${result.components.length}, 함수 ${result.functions.length}, API ${result.apiClients.length}, 라우트 ${result.routes.length}`);
81
+ await mkdir(opts.output, { recursive: true });
82
+ await writeFile(`${opts.output}/analysis.json`, JSON.stringify(result, null, 2));
83
+ // Step 2: 리포트
84
+ console.log(chalk.gray('━'.repeat(50)));
85
+ const reports = await generateReport(result, { outputDir: `${opts.output}/reports` });
86
+ console.log(chalk.green('✓'), '리포트 생성 완료!');
87
+ reports.forEach(p => console.log(` → ${chalk.cyan(p)}`));
88
+ // Step 3: 테스트
89
+ console.log(chalk.gray('━'.repeat(50)));
90
+ const tests = await generateTests(result, { outputDir: `${opts.output}/tests` });
91
+ console.log(chalk.green('✓'), `테스트 생성 완료! (${tests.length}개)`);
92
+ tests.forEach(p => console.log(` → ${chalk.cyan(p)}`));
93
+ console.log(chalk.green('\n✓'), '전체 파이프라인 완료!', chalk.cyan(opts.output), '\n');
94
+ });
95
+ program.parse();
@@ -0,0 +1,6 @@
1
+ import type { AnalysisResult } from '../types.js';
2
+ export interface ReportOptions {
3
+ formats?: ('excel' | 'mermaid')[];
4
+ outputDir?: string;
5
+ }
6
+ export declare function generateReport(data: AnalysisResult, options?: ReportOptions): Promise<string[]>;
@@ -0,0 +1,105 @@
1
+ import ExcelJS from 'exceljs';
2
+ import { writeFile, mkdir } from 'fs/promises';
3
+ export async function generateReport(data, options = {}) {
4
+ const formats = options.formats || ['excel', 'mermaid'];
5
+ const outputDir = options.outputDir || 'output/reports';
6
+ await mkdir(outputDir, { recursive: true });
7
+ const outputs = [];
8
+ if (formats.includes('excel')) {
9
+ outputs.push(await generateExcel(data, outputDir));
10
+ }
11
+ if (formats.includes('mermaid')) {
12
+ outputs.push(await generateMermaid(data, outputDir));
13
+ }
14
+ return outputs;
15
+ }
16
+ async function generateExcel(data, outDir) {
17
+ const wb = new ExcelJS.Workbook();
18
+ wb.creator = 'reverse-engine';
19
+ const hs = {
20
+ font: { bold: true, color: { argb: 'FFFFFFFF' }, size: 11 },
21
+ fill: { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF2B579A' } },
22
+ alignment: { horizontal: 'center', vertical: 'middle' },
23
+ };
24
+ // 컴포넌트
25
+ const s1 = wb.addWorksheet('컴포넌트 목록');
26
+ s1.columns = [
27
+ { header: 'No', width: 6 }, { header: '컴포넌트명', width: 20 },
28
+ { header: '파일 경로', width: 35 }, { header: '타입', width: 12 },
29
+ { header: '하위 컴포넌트', width: 30 }, { header: '사용처', width: 20 },
30
+ { header: 'Hooks', width: 25 }, { header: 'API 호출', width: 30 },
31
+ ];
32
+ s1.getRow(1).eachCell(c => { c.style = hs; });
33
+ data.components.forEach((c, i) => s1.addRow([
34
+ i + 1, c.name, c.filePath, c.componentType,
35
+ c.children.join(', '), c.usedBy.join(', '),
36
+ c.hooks.join(', '), c.apiCalls.join(', '),
37
+ ]));
38
+ // API
39
+ const s2 = wb.addWorksheet('API 엔드포인트');
40
+ s2.columns = [
41
+ { header: 'No', width: 6 }, { header: 'Method', width: 10 },
42
+ { header: 'URL', width: 35 }, { header: '파일', width: 35 },
43
+ { header: '함수', width: 25 }, { header: '라인', width: 8 },
44
+ ];
45
+ s2.getRow(1).eachCell(c => { c.style = hs; });
46
+ data.apiClients.forEach((a, i) => s2.addRow([i + 1, a.method, a.urlPattern, a.filePath, a.functionName, a.line]));
47
+ // 라우트
48
+ const s3 = wb.addWorksheet('라우트');
49
+ s3.columns = [
50
+ { header: 'No', width: 6 }, { header: 'Path', width: 25 },
51
+ { header: 'Component', width: 25 }, { header: '파일', width: 35 },
52
+ ];
53
+ s3.getRow(1).eachCell(c => { c.style = hs; });
54
+ data.routes.forEach((r, i) => s3.addRow([i + 1, r.path, r.component, r.filePath]));
55
+ // 함수
56
+ const s4 = wb.addWorksheet('함수 호출 체인');
57
+ s4.columns = [
58
+ { header: 'No', width: 6 }, { header: '함수명', width: 25 },
59
+ { header: '파일', width: 35 }, { header: 'Async', width: 8 },
60
+ { header: 'Export', width: 8 }, { header: '호출하는 함수', width: 40 },
61
+ { header: '호출되는 곳', width: 25 },
62
+ ];
63
+ s4.getRow(1).eachCell(c => { c.style = hs; });
64
+ data.functions.forEach((f, i) => s4.addRow([
65
+ i + 1, f.name, f.filePath, f.isAsync ? 'Y' : 'N', f.isExported ? 'Y' : 'N',
66
+ f.calls.join(', '), f.calledBy.join(', '),
67
+ ]));
68
+ // 의존성
69
+ const s5 = wb.addWorksheet('의존성 패키지');
70
+ s5.columns = [
71
+ { header: 'No', width: 6 }, { header: '패키지명', width: 30 },
72
+ { header: '현재 버전', width: 15 }, { header: '타입', width: 15 },
73
+ ];
74
+ s5.getRow(1).eachCell(c => { c.style = hs; });
75
+ data.dependencies.forEach((d, i) => s5.addRow([i + 1, d.name, d.currentVersion, d.depType]));
76
+ const path = `${outDir}/reverseng-report.xlsx`;
77
+ await wb.xlsx.writeFile(path);
78
+ return path;
79
+ }
80
+ async function generateMermaid(data, outDir) {
81
+ const cm = {};
82
+ let mm = 'graph TD\n';
83
+ data.components.forEach((c, i) => {
84
+ cm[c.name] = `C${i}`;
85
+ mm += ` C${i}["${c.name}"]\n`;
86
+ });
87
+ data.components.forEach((c, i) => {
88
+ c.children.forEach(ch => { if (cm[ch])
89
+ mm += ` C${i} --> ${cm[ch]}\n`; });
90
+ });
91
+ data.routes.forEach((r, i) => {
92
+ if (cm[r.component])
93
+ mm += ` R${i}("${r.path}") -.-> ${cm[r.component]}\n`;
94
+ });
95
+ mm += '\n classDef page fill:#e74c3c,color:#fff\n';
96
+ mm += ' classDef widget fill:#3498db,color:#fff\n';
97
+ mm += ' classDef layout fill:#2ecc71,color:#fff\n';
98
+ data.components.forEach((c, i) => {
99
+ const cls = c.componentType === 'Page' ? 'page' : c.componentType === 'Layout' ? 'layout' : 'widget';
100
+ mm += ` class C${i} ${cls}\n`;
101
+ });
102
+ const path = `${outDir}/component-graph.mmd`;
103
+ await writeFile(path, mm, 'utf-8');
104
+ return path;
105
+ }
@@ -0,0 +1,4 @@
1
+ export { analyze } from './analyzer/index.js';
2
+ export { generateReport } from './docgen/index.js';
3
+ export { generateTests } from './testgen/index.js';
4
+ export type * from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { analyze } from './analyzer/index.js';
2
+ export { generateReport } from './docgen/index.js';
3
+ export { generateTests } from './testgen/index.js';
@@ -0,0 +1,6 @@
1
+ import type { AnalysisResult } from '../types.js';
2
+ export interface TestGenOptions {
3
+ types?: ('e2e' | 'api')[];
4
+ outputDir?: string;
5
+ }
6
+ export declare function generateTests(data: AnalysisResult, options?: TestGenOptions): Promise<string[]>;
@@ -0,0 +1,80 @@
1
+ import { writeFile, mkdir } from 'fs/promises';
2
+ export async function generateTests(data, options = {}) {
3
+ const types = options.types || ['e2e', 'api'];
4
+ const outputDir = options.outputDir || 'output/tests';
5
+ const files = [];
6
+ if (types.includes('e2e'))
7
+ files.push(...await genE2E(data, outputDir));
8
+ if (types.includes('api'))
9
+ files.push(...await genAPI(data, outputDir));
10
+ return files;
11
+ }
12
+ async function genE2E(data, outDir) {
13
+ const files = [];
14
+ // 라우트 테스트
15
+ await mkdir(`${outDir}/e2e`, { recursive: true });
16
+ let code = `import { test, expect } from '@playwright/test';\n\n`;
17
+ for (const r of data.routes) {
18
+ code += `test.describe('${r.path} (${r.component})', () => {\n`;
19
+ code += ` test('페이지 로드', async ({ page }) => {\n`;
20
+ code += ` const response = await page.goto('${r.path}');\n`;
21
+ code += ` expect(response?.status()).toBeLessThan(400);\n`;
22
+ code += ` });\n});\n\n`;
23
+ }
24
+ const p1 = `${outDir}/e2e/routes.spec.ts`;
25
+ await writeFile(p1, code);
26
+ files.push(p1);
27
+ // 페이지 컴포넌트 테스트
28
+ await mkdir(`${outDir}/components`, { recursive: true });
29
+ for (const c of data.components.filter(c => c.componentType === 'Page')) {
30
+ const route = data.routes.find(r => r.component === c.name);
31
+ const url = route?.path || '/';
32
+ let t = `import { test, expect } from '@playwright/test';\n\n`;
33
+ t += `test.describe('${c.name}', () => {\n`;
34
+ t += ` test('렌더링 확인', async ({ page }) => {\n`;
35
+ t += ` await page.goto('${url}');\n`;
36
+ for (const ch of c.children)
37
+ t += ` // ${ch} 컴포넌트 확인\n`;
38
+ t += ` });\n`;
39
+ for (const api of c.apiCalls) {
40
+ t += `\n test('API: ${api}', async ({ page }) => {\n`;
41
+ t += ` await page.goto('${url}');\n`;
42
+ t += ` // TODO: ${api} 검증\n`;
43
+ t += ` });\n`;
44
+ }
45
+ t += `});\n`;
46
+ const p = `${outDir}/components/${c.name}.spec.ts`;
47
+ await writeFile(p, t);
48
+ files.push(p);
49
+ }
50
+ return files;
51
+ }
52
+ async function genAPI(data, outDir) {
53
+ await mkdir(`${outDir}/api`, { recursive: true });
54
+ let code = `import { test, expect } from '@playwright/test';\n\n`;
55
+ code += `test.describe('API 엔드포인트', () => {\n`;
56
+ for (const a of data.apiClients) {
57
+ code += ` test('${a.method} ${a.urlPattern}', async ({ request }) => {\n`;
58
+ switch (a.method) {
59
+ case 'GET':
60
+ code += ` const response = await request.get('${a.urlPattern}');\n`;
61
+ break;
62
+ case 'POST':
63
+ code += ` const response = await request.post('${a.urlPattern}', { data: {} });\n`;
64
+ break;
65
+ case 'PUT':
66
+ code += ` const response = await request.put('${a.urlPattern}', { data: {} });\n`;
67
+ break;
68
+ case 'DELETE':
69
+ code += ` const response = await request.delete('${a.urlPattern}');\n`;
70
+ break;
71
+ default: code += ` const response = await request.fetch('${a.urlPattern}', { method: '${a.method}' });\n`;
72
+ }
73
+ code += ` expect(response.status()).toBeLessThan(500);\n`;
74
+ code += ` });\n\n`;
75
+ }
76
+ code += `});\n`;
77
+ const p = `${outDir}/api/endpoints.spec.ts`;
78
+ await writeFile(p, code);
79
+ return [p];
80
+ }
@@ -0,0 +1,102 @@
1
+ /** 분석 결과 전체 */
2
+ export interface AnalysisResult {
3
+ sourcePath: string;
4
+ framework: string;
5
+ components: ComponentInfo[];
6
+ routes: RouteInfo[];
7
+ functions: FunctionInfo[];
8
+ apiClients: ApiClientCall[];
9
+ dependencies: DependencyInfo[];
10
+ }
11
+ export interface ComponentInfo {
12
+ name: string;
13
+ filePath: string;
14
+ lineStart: number;
15
+ lineEnd: number;
16
+ componentType: 'Page' | 'Layout' | 'Widget' | 'Utility';
17
+ props: PropInfo[];
18
+ children: string[];
19
+ usedBy: string[];
20
+ hooks: string[];
21
+ apiCalls: string[];
22
+ }
23
+ export interface PropInfo {
24
+ name: string;
25
+ type: string;
26
+ required: boolean;
27
+ defaultValue?: string;
28
+ }
29
+ export interface RouteInfo {
30
+ path: string;
31
+ component: string;
32
+ filePath: string;
33
+ guards: string[];
34
+ meta?: Record<string, unknown>;
35
+ }
36
+ export interface FunctionInfo {
37
+ name: string;
38
+ filePath: string;
39
+ lineStart: number;
40
+ lineEnd: number;
41
+ params: string[];
42
+ returnType?: string;
43
+ calls: string[];
44
+ calledBy: string[];
45
+ isAsync: boolean;
46
+ isExported: boolean;
47
+ }
48
+ export interface ApiClientCall {
49
+ method: string;
50
+ urlPattern: string;
51
+ filePath: string;
52
+ line: number;
53
+ functionName: string;
54
+ }
55
+ export interface DependencyInfo {
56
+ name: string;
57
+ currentVersion: string;
58
+ latestVersion?: string;
59
+ depType: 'Production' | 'Development' | 'Peer';
60
+ license?: string;
61
+ }
62
+ /** 크롤링 결과 */
63
+ export interface CrawlResult {
64
+ targetUrl: string;
65
+ pages: PageInfo[];
66
+ timestamp: string;
67
+ }
68
+ export interface PageInfo {
69
+ url: string;
70
+ title: string;
71
+ screenshotPath: string | null;
72
+ elements: {
73
+ links: {
74
+ text: string;
75
+ href: string;
76
+ selector: string;
77
+ }[];
78
+ buttons: {
79
+ text: string;
80
+ selector: string;
81
+ navigatesTo: string | null;
82
+ }[];
83
+ forms: {
84
+ id: string | null;
85
+ action: string | null;
86
+ method: string;
87
+ fields: {
88
+ name: string;
89
+ fieldType: string;
90
+ required: boolean;
91
+ }[];
92
+ }[];
93
+ };
94
+ apiCalls: {
95
+ method: string;
96
+ url: string;
97
+ responseStatus: number;
98
+ triggeredBy: string | null;
99
+ }[];
100
+ navigatesTo: string[];
101
+ authRequired: boolean;
102
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "reverse-engine",
3
+ "version": "0.1.0",
4
+ "description": "웹 서비스 역분석 자동화 도구 - 소스코드 분석, 문서 생성, 테스트 자동화",
5
+ "keywords": [
6
+ "reverse-engineering",
7
+ "code-analysis",
8
+ "documentation",
9
+ "test-generation",
10
+ "crawler",
11
+ "static-analysis",
12
+ "ast",
13
+ "react",
14
+ "vue",
15
+ "angular",
16
+ "excel"
17
+ ],
18
+ "author": "injaehwang",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/injaehwang/revers-eng"
23
+ },
24
+ "type": "module",
25
+ "bin": {
26
+ "reverse-engine": "dist/cli/index.js",
27
+ "reverseng": "dist/cli/index.js"
28
+ },
29
+ "main": "dist/index.js",
30
+ "types": "dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "import": "./dist/index.js",
34
+ "types": "./dist/index.d.ts"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "README.md"
40
+ ],
41
+ "scripts": {
42
+ "build": "tsc",
43
+ "dev": "tsx src/cli/index.ts",
44
+ "prepublishOnly": "npm run build",
45
+ "test": "node --test dist/**/*.test.js"
46
+ },
47
+ "dependencies": {
48
+ "@babel/parser": "^7.26.0",
49
+ "@babel/traverse": "^7.26.0",
50
+ "@babel/types": "^7.26.0",
51
+ "commander": "^13.0.0",
52
+ "exceljs": "^4.4.0",
53
+ "chalk": "^5.4.0",
54
+ "ora": "^8.2.0",
55
+ "playwright": "^1.49.0",
56
+ "glob": "^11.0.0"
57
+ },
58
+ "devDependencies": {
59
+ "@types/babel__traverse": "^7.20.0",
60
+ "@types/node": "^22.0.0",
61
+ "typescript": "^5.7.0",
62
+ "tsx": "^4.19.0"
63
+ },
64
+ "engines": {
65
+ "node": ">=18.0.0"
66
+ }
67
+ }