@mandujs/ate 0.17.0 → 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/codegen.ts +67 -37
- package/src/dep-graph.ts +2 -2
- package/src/extractor.ts +67 -34
- package/src/fs.ts +20 -14
- package/src/impact.ts +12 -12
- package/src/pipeline.ts +26 -20
- package/src/report.ts +8 -8
- package/src/reporter/html.ts +6 -6
- package/src/runner.ts +6 -6
- package/src/scenario.ts +17 -12
- package/src/ts-morph-types.ts +60 -0
- package/src/types.ts +1 -1
package/package.json
CHANGED
package/src/codegen.ts
CHANGED
|
@@ -47,8 +47,8 @@ export function generatePlaywrightSpecs(repoRoot: string, opts?: { onlyRoutes?:
|
|
|
47
47
|
let bundle: ScenarioBundle;
|
|
48
48
|
try {
|
|
49
49
|
bundle = readJson<ScenarioBundle>(paths.scenariosPath);
|
|
50
|
-
} catch (err:
|
|
51
|
-
throw new Error(`시나리오 번들 읽기 실패: ${err.message}`);
|
|
50
|
+
} catch (err: unknown) {
|
|
51
|
+
throw new Error(`시나리오 번들 읽기 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (!bundle.scenarios || bundle.scenarios.length === 0) {
|
|
@@ -59,15 +59,15 @@ export function generatePlaywrightSpecs(repoRoot: string, opts?: { onlyRoutes?:
|
|
|
59
59
|
let selectorMap;
|
|
60
60
|
try {
|
|
61
61
|
selectorMap = readSelectorMap(repoRoot);
|
|
62
|
-
} catch (err:
|
|
62
|
+
} catch (err: unknown) {
|
|
63
63
|
// Selector map is optional
|
|
64
|
-
warnings.push(`Selector map 읽기 실패 (무시): ${err.message}`);
|
|
64
|
+
warnings.push(`Selector map 읽기 실패 (무시): ${err instanceof Error ? err.message : String(err)}`);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
try {
|
|
68
68
|
ensureDir(paths.autoE2eDir);
|
|
69
|
-
} catch (err:
|
|
70
|
-
throw new Error(`E2E 디렉토리 생성 실패: ${err.message}`);
|
|
69
|
+
} catch (err: unknown) {
|
|
70
|
+
throw new Error(`E2E 디렉토리 생성 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
const files: string[] = [];
|
|
@@ -78,40 +78,69 @@ export function generatePlaywrightSpecs(repoRoot: string, opts?: { onlyRoutes?:
|
|
|
78
78
|
const safeId = s.id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
79
79
|
const filePath = join(paths.autoE2eDir, `${safeId}.spec.ts`);
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
let code: string;
|
|
82
|
+
|
|
83
|
+
if (s.kind === "api-smoke") {
|
|
84
|
+
// API route: fetch-based test
|
|
85
|
+
const methods = s.methods ?? ["GET"];
|
|
86
|
+
const testCases = methods.map((method) => {
|
|
87
|
+
return [
|
|
88
|
+
` test(${JSON.stringify(`${method} ${s.route}`)}, async ({ baseURL }) => {`,
|
|
89
|
+
` const url = (baseURL ?? "http://localhost:3333") + ${JSON.stringify(s.route)};`,
|
|
90
|
+
` const res = await fetch(url, { method: ${JSON.stringify(method)} });`,
|
|
91
|
+
` expect(res.status).toBeLessThan(500);`,
|
|
92
|
+
` expect(res.headers.get("content-type")).toBeTruthy();`,
|
|
93
|
+
method === "GET" ? ` const body = await res.text();\n expect(body.length).toBeGreaterThan(0);` : "",
|
|
94
|
+
` });`,
|
|
95
|
+
].filter(Boolean).join("\n");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
code = [
|
|
99
|
+
specHeader(),
|
|
100
|
+
`test.describe(${JSON.stringify(s.id)}, () => {`,
|
|
101
|
+
...testCases,
|
|
102
|
+
`});`,
|
|
103
|
+
"",
|
|
104
|
+
].join("\n");
|
|
105
|
+
} else {
|
|
106
|
+
// Page route: browser-based test
|
|
107
|
+
const oracle = oracleTemplate(s.oracleLevel, s.route);
|
|
108
|
+
|
|
109
|
+
// Generate selector examples if selector map exists
|
|
110
|
+
let selectorExamples = "";
|
|
111
|
+
if (selectorMap && selectorMap.entries.length > 0) {
|
|
112
|
+
const exampleEntry = selectorMap.entries[0];
|
|
113
|
+
const locatorChain = buildPlaywrightLocatorChain(exampleEntry);
|
|
114
|
+
selectorExamples = ` // Example: Selector with fallback chain\n // const loginBtn = ${locatorChain};\n`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
code = [
|
|
118
|
+
specHeader(),
|
|
119
|
+
`test.describe(${JSON.stringify(s.id)}, () => {`,
|
|
120
|
+
` test(${JSON.stringify(`smoke ${s.route}`)}, async ({ page, baseURL }) => {`,
|
|
121
|
+
` const url = (baseURL ?? "http://localhost:3333") + ${JSON.stringify(s.route === "/" ? "/" : s.route)};`,
|
|
122
|
+
` ${oracle.setup.split("\n").join("\n ")}`,
|
|
123
|
+
` await page.goto(url);`,
|
|
124
|
+
selectorExamples,
|
|
125
|
+
` ${oracle.assertions.split("\n").join("\n ")}`,
|
|
126
|
+
` });`,
|
|
127
|
+
`});`,
|
|
128
|
+
"",
|
|
129
|
+
].join("\n");
|
|
89
130
|
}
|
|
90
131
|
|
|
91
|
-
const code = [
|
|
92
|
-
specHeader(),
|
|
93
|
-
`test.describe(${JSON.stringify(s.id)}, () => {`,
|
|
94
|
-
` test(${JSON.stringify(`smoke ${s.route}`)}, async ({ page, baseURL }) => {`,
|
|
95
|
-
` const url = (baseURL ?? "http://localhost:3333") + ${JSON.stringify(s.route === "/" ? "/" : s.route)};`,
|
|
96
|
-
` ${oracle.setup.split("\n").join("\n ")}`,
|
|
97
|
-
` await page.goto(url);`,
|
|
98
|
-
selectorExamples,
|
|
99
|
-
` ${oracle.assertions.split("\n").join("\n ")}`,
|
|
100
|
-
` });`,
|
|
101
|
-
`});`,
|
|
102
|
-
"",
|
|
103
|
-
].join("\n");
|
|
104
|
-
|
|
105
132
|
try {
|
|
106
133
|
writeFileSync(filePath, code, "utf8");
|
|
107
134
|
files.push(filePath);
|
|
108
|
-
} catch (err:
|
|
109
|
-
|
|
110
|
-
|
|
135
|
+
} catch (err: unknown) {
|
|
136
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
137
|
+
warnings.push(`Spec 파일 쓰기 실패 (${filePath}): ${msg}`);
|
|
138
|
+
console.error(`[ATE] Spec 생성 실패: ${filePath} - ${msg}`);
|
|
111
139
|
}
|
|
112
|
-
} catch (err:
|
|
113
|
-
|
|
114
|
-
|
|
140
|
+
} catch (err: unknown) {
|
|
141
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
142
|
+
warnings.push(`Spec 생성 실패 (${s.id}): ${msg}`);
|
|
143
|
+
console.error(`[ATE] Spec 생성 에러: ${s.id} - ${msg}`);
|
|
115
144
|
// Continue with next scenario
|
|
116
145
|
}
|
|
117
146
|
}
|
|
@@ -131,9 +160,10 @@ export function generatePlaywrightSpecs(repoRoot: string, opts?: { onlyRoutes?:
|
|
|
131
160
|
Bun.write(configPath, desiredConfig);
|
|
132
161
|
}
|
|
133
162
|
}
|
|
134
|
-
} catch (err:
|
|
135
|
-
|
|
136
|
-
|
|
163
|
+
} catch (err: unknown) {
|
|
164
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
165
|
+
warnings.push(`Playwright config 생성 실패: ${msg}`);
|
|
166
|
+
console.warn(`[ATE] Playwright config 생성 실패: ${msg}`);
|
|
137
167
|
}
|
|
138
168
|
|
|
139
169
|
return { files, warnings };
|
package/src/dep-graph.ts
CHANGED
|
@@ -38,10 +38,10 @@ function normalizePath(path: string, rootDir: string): string {
|
|
|
38
38
|
* Resolve import specifier to file path
|
|
39
39
|
*/
|
|
40
40
|
function resolveImport(
|
|
41
|
-
sourceFile:
|
|
41
|
+
sourceFile: import("./ts-morph-types").SourceFile,
|
|
42
42
|
importSpecifier: string,
|
|
43
43
|
rootDir: string,
|
|
44
|
-
project:
|
|
44
|
+
project: import("./ts-morph-types").Project
|
|
45
45
|
): string | null {
|
|
46
46
|
// Skip external modules (no relative/absolute path)
|
|
47
47
|
if (!importSpecifier.startsWith(".") && !importSpecifier.startsWith("/")) {
|
package/src/extractor.ts
CHANGED
|
@@ -3,18 +3,24 @@ import { relative, join } from "node:path";
|
|
|
3
3
|
import { createEmptyGraph, addEdge, addNode } from "./ir";
|
|
4
4
|
import { getAtePaths, writeJson } from "./fs";
|
|
5
5
|
import type { ExtractInput, InteractionGraph } from "./types";
|
|
6
|
+
import type { Node, CallExpression, JsxAttribute, SyntaxKindEnum } from "./ts-morph-types";
|
|
6
7
|
|
|
7
|
-
const DEFAULT_ROUTE_GLOBS = [
|
|
8
|
+
const DEFAULT_ROUTE_GLOBS = [
|
|
9
|
+
"app/**/page.tsx",
|
|
10
|
+
"app/**/route.ts",
|
|
11
|
+
"routes/**/page.tsx",
|
|
12
|
+
"routes/**/route.ts",
|
|
13
|
+
];
|
|
8
14
|
|
|
9
|
-
function isStringLiteral(node:
|
|
10
|
-
return node.getKind() ===
|
|
15
|
+
function isStringLiteral(node: Node, SK: SyntaxKindEnum): boolean {
|
|
16
|
+
return node.getKind() === SK.StringLiteral;
|
|
11
17
|
}
|
|
12
18
|
|
|
13
|
-
function tryExtractLiteralArg(callExpr:
|
|
19
|
+
function tryExtractLiteralArg(callExpr: CallExpression, argIndex = 0, SK: SyntaxKindEnum): string | null {
|
|
14
20
|
const args = callExpr.getArguments();
|
|
15
21
|
const arg = args[argIndex];
|
|
16
22
|
if (!arg) return null;
|
|
17
|
-
if (isStringLiteral(arg,
|
|
23
|
+
if (isStringLiteral(arg, SK)) return (arg as Node & { getLiteralValue(): string }).getLiteralValue();
|
|
18
24
|
return null;
|
|
19
25
|
}
|
|
20
26
|
|
|
@@ -41,8 +47,8 @@ export async function extract(input: ExtractInput): Promise<{ ok: true; graphPat
|
|
|
41
47
|
onlyFiles: true,
|
|
42
48
|
ignore: ["**/node_modules/**", "**/.mandu/**"],
|
|
43
49
|
});
|
|
44
|
-
} catch (err:
|
|
45
|
-
throw new Error(`파일 검색 실패: ${err.message}`);
|
|
50
|
+
} catch (err: unknown) {
|
|
51
|
+
throw new Error(`파일 검색 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
if (routeFiles.length === 0) {
|
|
@@ -51,15 +57,16 @@ export async function extract(input: ExtractInput): Promise<{ ok: true; graphPat
|
|
|
51
57
|
|
|
52
58
|
// Lazy load ts-morph only when needed
|
|
53
59
|
const { Project, SyntaxKind } = await import("ts-morph");
|
|
60
|
+
const SK = SyntaxKind as unknown as SyntaxKindEnum;
|
|
54
61
|
|
|
55
|
-
let project:
|
|
62
|
+
let project: InstanceType<typeof Project>;
|
|
56
63
|
try {
|
|
57
64
|
project = new Project({
|
|
58
65
|
tsConfigFilePath: input.tsconfigPath ? join(repoRoot, input.tsconfigPath) : undefined,
|
|
59
66
|
skipAddingFilesFromTsConfig: true,
|
|
60
67
|
});
|
|
61
|
-
} catch (err:
|
|
62
|
-
throw new Error(`TypeScript 프로젝트 초기화 실패: ${err.message}`);
|
|
68
|
+
} catch (err: unknown) {
|
|
69
|
+
throw new Error(`TypeScript 프로젝트 초기화 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
for (const filePath of routeFiles) {
|
|
@@ -68,86 +75,112 @@ export async function extract(input: ExtractInput): Promise<{ ok: true; graphPat
|
|
|
68
75
|
const rel = relative(repoRoot, filePath);
|
|
69
76
|
const relNormalized = rel.replace(/\\/g, "/");
|
|
70
77
|
|
|
71
|
-
|
|
78
|
+
const isApiRoute = relNormalized.endsWith("/route.ts");
|
|
79
|
+
|
|
80
|
+
// route node id: normalize to path without trailing /page.tsx or /route.ts
|
|
72
81
|
const routePath = relNormalized
|
|
73
82
|
.replace(/^app\//, "/")
|
|
74
83
|
.replace(/^routes\//, "/")
|
|
75
84
|
.replace(/\/page\.tsx$/, "")
|
|
85
|
+
.replace(/\/route\.ts$/, "")
|
|
76
86
|
.replace(/\/index\.tsx$/, "")
|
|
77
87
|
.replace(/\/page$/, "")
|
|
78
88
|
.replace(/\\/g, "/");
|
|
79
89
|
|
|
80
|
-
|
|
90
|
+
// API route: extract HTTP methods from exports (GET, POST, PUT, PATCH, DELETE)
|
|
91
|
+
let methods: string[] = [];
|
|
92
|
+
if (isApiRoute) {
|
|
93
|
+
const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
94
|
+
const exportDecls = sourceFile.getExportedDeclarations();
|
|
95
|
+
for (const [name] of exportDecls) {
|
|
96
|
+
if (HTTP_METHODS.includes(name)) {
|
|
97
|
+
methods.push(name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (methods.length === 0) methods = ["GET"]; // default
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
addNode(graph, {
|
|
104
|
+
kind: "route",
|
|
105
|
+
id: routePath === "" ? "/" : routePath,
|
|
106
|
+
file: relNormalized,
|
|
107
|
+
path: routePath === "" ? "/" : routePath,
|
|
108
|
+
...(isApiRoute ? { methods } : {}),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// API route에는 JSX/navigation이 없으므로 건너뜀
|
|
112
|
+
if (isApiRoute) continue;
|
|
81
113
|
|
|
82
114
|
// ManduLink / Link literal extraction: <Link href="/x"> or <ManduLink to="/x">
|
|
83
115
|
try {
|
|
84
|
-
const jsxAttrs = sourceFile.getDescendantsOfKind(SyntaxKind.JsxAttribute);
|
|
116
|
+
const jsxAttrs = sourceFile.getDescendantsOfKind(SyntaxKind.JsxAttribute) as unknown as JsxAttribute[];
|
|
85
117
|
for (const attr of jsxAttrs) {
|
|
86
118
|
try {
|
|
87
|
-
const name =
|
|
119
|
+
const name = attr.getNameNode?.().getText?.() ?? attr.getName?.() ?? "";
|
|
88
120
|
if (name !== "to" && name !== "href") continue;
|
|
89
|
-
const init =
|
|
121
|
+
const init = attr.getInitializer?.();
|
|
90
122
|
if (!init) continue;
|
|
91
|
-
if (init.getKind?.() ===
|
|
92
|
-
const raw =
|
|
123
|
+
if (init.getKind?.() === SK.StringLiteral) {
|
|
124
|
+
const raw = init.getLiteralValue?.() ?? init.getText?.();
|
|
93
125
|
const to = typeof raw === "string" ? raw.replace(/^"|"$/g, "") : null;
|
|
94
126
|
if (typeof to === "string" && to.startsWith("/")) {
|
|
95
127
|
addEdge(graph, { kind: "navigate", from: routePath || "/", to, file: relNormalized, source: `<jsx ${name}>` });
|
|
96
128
|
}
|
|
97
129
|
}
|
|
98
|
-
} catch (err:
|
|
130
|
+
} catch (err: unknown) {
|
|
99
131
|
// Skip invalid JSX attributes
|
|
100
|
-
warnings.push(`JSX 속성 파싱 실패 (${relNormalized}): ${err.message}`);
|
|
132
|
+
warnings.push(`JSX 속성 파싱 실패 (${relNormalized}): ${err instanceof Error ? err.message : String(err)}`);
|
|
101
133
|
}
|
|
102
134
|
}
|
|
103
|
-
} catch (err:
|
|
104
|
-
warnings.push(`JSX 분석 실패 (${relNormalized}): ${err.message}`);
|
|
135
|
+
} catch (err: unknown) {
|
|
136
|
+
warnings.push(`JSX 분석 실패 (${relNormalized}): ${err instanceof Error ? err.message : String(err)}`);
|
|
105
137
|
}
|
|
106
138
|
|
|
107
139
|
// mandu.navigate("/x") literal
|
|
108
140
|
try {
|
|
109
|
-
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
141
|
+
const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression) as unknown as CallExpression[];
|
|
110
142
|
for (const call of calls) {
|
|
111
143
|
try {
|
|
112
144
|
const exprText = call.getExpression().getText();
|
|
113
145
|
if (exprText === "mandu.navigate" || exprText.endsWith(".navigate")) {
|
|
114
|
-
const to = tryExtractLiteralArg(call, 0,
|
|
146
|
+
const to = tryExtractLiteralArg(call, 0, SK);
|
|
115
147
|
if (to && to.startsWith("/")) {
|
|
116
148
|
addEdge(graph, { kind: "navigate", from: routePath || "/", to, file: relNormalized, source: "mandu.navigate" });
|
|
117
149
|
}
|
|
118
150
|
}
|
|
119
151
|
if (exprText === "mandu.modal.open" || exprText.endsWith(".modal.open")) {
|
|
120
|
-
const modal = tryExtractLiteralArg(call, 0,
|
|
152
|
+
const modal = tryExtractLiteralArg(call, 0, SK);
|
|
121
153
|
if (modal) {
|
|
122
154
|
addEdge(graph, { kind: "openModal", from: routePath || "/", modal, file: relNormalized, source: "mandu.modal.open" });
|
|
123
155
|
}
|
|
124
156
|
}
|
|
125
157
|
if (exprText === "mandu.action.run" || exprText.endsWith(".action.run")) {
|
|
126
|
-
const action = tryExtractLiteralArg(call, 0,
|
|
158
|
+
const action = tryExtractLiteralArg(call, 0, SK);
|
|
127
159
|
if (action) {
|
|
128
160
|
addEdge(graph, { kind: "runAction", from: routePath || "/", action, file: relNormalized, source: "mandu.action.run" });
|
|
129
161
|
}
|
|
130
162
|
}
|
|
131
|
-
} catch (err:
|
|
163
|
+
} catch (err: unknown) {
|
|
132
164
|
// Skip invalid call expressions
|
|
133
|
-
warnings.push(`함수 호출 파싱 실패 (${relNormalized}): ${err.message}`);
|
|
165
|
+
warnings.push(`함수 호출 파싱 실패 (${relNormalized}): ${err instanceof Error ? err.message : String(err)}`);
|
|
134
166
|
}
|
|
135
167
|
}
|
|
136
|
-
} catch (err:
|
|
137
|
-
warnings.push(`함수 호출 분석 실패 (${relNormalized}): ${err.message}`);
|
|
168
|
+
} catch (err: unknown) {
|
|
169
|
+
warnings.push(`함수 호출 분석 실패 (${relNormalized}): ${err instanceof Error ? err.message : String(err)}`);
|
|
138
170
|
}
|
|
139
|
-
} catch (err:
|
|
171
|
+
} catch (err: unknown) {
|
|
140
172
|
// Graceful degradation: skip this file and continue
|
|
141
|
-
|
|
142
|
-
|
|
173
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
174
|
+
warnings.push(`파일 파싱 실패 (${filePath}): ${msg}`);
|
|
175
|
+
console.warn(`[ATE] 파일 스킵: ${filePath} - ${msg}`);
|
|
143
176
|
continue;
|
|
144
177
|
}
|
|
145
178
|
}
|
|
146
179
|
|
|
147
180
|
try {
|
|
148
181
|
writeJson(paths.interactionGraphPath, graph);
|
|
149
|
-
} catch (err:
|
|
150
|
-
throw new Error(`Interaction graph 저장 실패: ${err.message}`);
|
|
182
|
+
} catch (err: unknown) {
|
|
183
|
+
throw new Error(`Interaction graph 저장 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
151
184
|
}
|
|
152
185
|
|
|
153
186
|
return {
|
package/src/fs.ts
CHANGED
|
@@ -34,15 +34,17 @@ export function getAtePaths(repoRoot: string): AtePaths {
|
|
|
34
34
|
export function ensureDir(path: string): void {
|
|
35
35
|
try {
|
|
36
36
|
mkdirSync(path, { recursive: true });
|
|
37
|
-
} catch (err:
|
|
38
|
-
|
|
37
|
+
} catch (err: unknown) {
|
|
38
|
+
const isNodeError = err instanceof Error && "code" in err;
|
|
39
|
+
const code = isNodeError ? (err as NodeJS.ErrnoException).code : undefined;
|
|
40
|
+
if (code === "EACCES") {
|
|
39
41
|
throw new ATEFileError(
|
|
40
42
|
`디렉토리 생성 권한이 없습니다: ${path}`,
|
|
41
43
|
"PERMISSION_DENIED",
|
|
42
44
|
path,
|
|
43
45
|
);
|
|
44
46
|
}
|
|
45
|
-
if (
|
|
47
|
+
if (code === "ENOSPC") {
|
|
46
48
|
throw new ATEFileError(
|
|
47
49
|
`디스크 공간이 부족합니다: ${path}`,
|
|
48
50
|
"NO_SPACE",
|
|
@@ -50,8 +52,8 @@ export function ensureDir(path: string): void {
|
|
|
50
52
|
);
|
|
51
53
|
}
|
|
52
54
|
throw new ATEFileError(
|
|
53
|
-
`디렉토리 생성 실패: ${path} (${err.message})`,
|
|
54
|
-
|
|
55
|
+
`디렉토리 생성 실패: ${path} (${err instanceof Error ? err.message : String(err)})`,
|
|
56
|
+
code || "UNKNOWN",
|
|
55
57
|
path,
|
|
56
58
|
);
|
|
57
59
|
}
|
|
@@ -65,18 +67,20 @@ export function writeJson(path: string, data: unknown): void {
|
|
|
65
67
|
try {
|
|
66
68
|
ensureDir(dirname(path));
|
|
67
69
|
writeFileSync(path, JSON.stringify(data, null, 2), "utf8");
|
|
68
|
-
} catch (err:
|
|
70
|
+
} catch (err: unknown) {
|
|
69
71
|
if (err instanceof ATEFileError) {
|
|
70
72
|
throw err;
|
|
71
73
|
}
|
|
72
|
-
|
|
74
|
+
const isNodeError = err instanceof Error && "code" in err;
|
|
75
|
+
const code = isNodeError ? (err as NodeJS.ErrnoException).code : undefined;
|
|
76
|
+
if (code === "EACCES") {
|
|
73
77
|
throw new ATEFileError(
|
|
74
78
|
`파일 쓰기 권한이 없습니다: ${path}`,
|
|
75
79
|
"PERMISSION_DENIED",
|
|
76
80
|
path,
|
|
77
81
|
);
|
|
78
82
|
}
|
|
79
|
-
if (
|
|
83
|
+
if (code === "ENOSPC") {
|
|
80
84
|
throw new ATEFileError(
|
|
81
85
|
`디스크 공간이 부족합니다: ${path}`,
|
|
82
86
|
"NO_SPACE",
|
|
@@ -84,8 +88,8 @@ export function writeJson(path: string, data: unknown): void {
|
|
|
84
88
|
);
|
|
85
89
|
}
|
|
86
90
|
throw new ATEFileError(
|
|
87
|
-
`JSON 파일 쓰기 실패: ${path} (${err.message})`,
|
|
88
|
-
|
|
91
|
+
`JSON 파일 쓰기 실패: ${path} (${err instanceof Error ? err.message : String(err)})`,
|
|
92
|
+
code || "UNKNOWN",
|
|
89
93
|
path,
|
|
90
94
|
);
|
|
91
95
|
}
|
|
@@ -107,11 +111,13 @@ export function readJson<T>(path: string): T {
|
|
|
107
111
|
|
|
108
112
|
const content = readFileSync(path, "utf8");
|
|
109
113
|
return JSON.parse(content) as T;
|
|
110
|
-
} catch (err:
|
|
114
|
+
} catch (err: unknown) {
|
|
111
115
|
if (err instanceof ATEFileError) {
|
|
112
116
|
throw err;
|
|
113
117
|
}
|
|
114
|
-
|
|
118
|
+
const isNodeError = err instanceof Error && "code" in err;
|
|
119
|
+
const code = isNodeError ? (err as NodeJS.ErrnoException).code : undefined;
|
|
120
|
+
if (code === "EACCES") {
|
|
115
121
|
throw new ATEFileError(
|
|
116
122
|
`파일 읽기 권한이 없습니다: ${path}`,
|
|
117
123
|
"PERMISSION_DENIED",
|
|
@@ -126,8 +132,8 @@ export function readJson<T>(path: string): T {
|
|
|
126
132
|
);
|
|
127
133
|
}
|
|
128
134
|
throw new ATEFileError(
|
|
129
|
-
`JSON 파일 읽기 실패: ${path} (${err.message})`,
|
|
130
|
-
|
|
135
|
+
`JSON 파일 읽기 실패: ${path} (${err instanceof Error ? err.message : String(err)})`,
|
|
136
|
+
code || "UNKNOWN",
|
|
131
137
|
path,
|
|
132
138
|
);
|
|
133
139
|
}
|
package/src/impact.ts
CHANGED
|
@@ -39,14 +39,14 @@ export async function computeImpact(input: ImpactInput): Promise<{ changedFiles:
|
|
|
39
39
|
// Verify git revisions
|
|
40
40
|
try {
|
|
41
41
|
verifyGitRev(repoRoot, base);
|
|
42
|
-
} catch (err:
|
|
43
|
-
throw new Error(`잘못된 base revision: ${base} (${err.message})`);
|
|
42
|
+
} catch (err: unknown) {
|
|
43
|
+
throw new Error(`잘못된 base revision: ${base} (${err instanceof Error ? err.message : String(err)})`);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
try {
|
|
47
47
|
verifyGitRev(repoRoot, head);
|
|
48
|
-
} catch (err:
|
|
49
|
-
throw new Error(`잘못된 head revision: ${head} (${err.message})`);
|
|
48
|
+
} catch (err: unknown) {
|
|
49
|
+
throw new Error(`잘못된 head revision: ${head} (${err instanceof Error ? err.message : String(err)})`);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
let out: string;
|
|
@@ -55,8 +55,8 @@ export async function computeImpact(input: ImpactInput): Promise<{ changedFiles:
|
|
|
55
55
|
cwd: repoRoot,
|
|
56
56
|
stdio: ["ignore", "pipe", "pipe"],
|
|
57
57
|
}).toString("utf8");
|
|
58
|
-
} catch (err:
|
|
59
|
-
throw new Error(`Git diff 실행 실패: ${err.message}`);
|
|
58
|
+
} catch (err: unknown) {
|
|
59
|
+
throw new Error(`Git diff 실행 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
const changedFiles = out.split("\n").map((s) => toPosixPath(s.trim())).filter(Boolean);
|
|
@@ -71,8 +71,8 @@ export async function computeImpact(input: ImpactInput): Promise<{ changedFiles:
|
|
|
71
71
|
let graph: InteractionGraph;
|
|
72
72
|
try {
|
|
73
73
|
graph = readJson<InteractionGraph>(paths.interactionGraphPath);
|
|
74
|
-
} catch (err:
|
|
75
|
-
throw new Error(`Interaction graph 읽기 실패: ${err.message}`);
|
|
74
|
+
} catch (err: unknown) {
|
|
75
|
+
throw new Error(`Interaction graph 읽기 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
if (!graph.nodes || graph.nodes.length === 0) {
|
|
@@ -95,8 +95,8 @@ export async function computeImpact(input: ImpactInput): Promise<{ changedFiles:
|
|
|
95
95
|
include: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
|
|
96
96
|
exclude: ["**/node_modules/**", "**/*.test.ts", "**/*.spec.ts"],
|
|
97
97
|
});
|
|
98
|
-
} catch (err:
|
|
99
|
-
warnings.push(`Dependency graph 빌드 실패: ${err.message}`);
|
|
98
|
+
} catch (err: unknown) {
|
|
99
|
+
warnings.push(`Dependency graph 빌드 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
100
100
|
// Fallback: only direct file matching
|
|
101
101
|
const selected = new Set<string>();
|
|
102
102
|
for (const changedFile of changedFiles) {
|
|
@@ -136,8 +136,8 @@ export async function computeImpact(input: ImpactInput): Promise<{ changedFiles:
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
-
} catch (err:
|
|
140
|
-
warnings.push(`파일 영향 분석 실패 (${changedFile}): ${err.message}`);
|
|
139
|
+
} catch (err: unknown) {
|
|
140
|
+
warnings.push(`파일 영향 분석 실패 (${changedFile}): ${err instanceof Error ? err.message : String(err)}`);
|
|
141
141
|
// Continue with next file
|
|
142
142
|
}
|
|
143
143
|
}
|
package/src/pipeline.ts
CHANGED
|
@@ -57,9 +57,10 @@ export async function runFullPipeline(options: AutoPipelineOptions): Promise<Aut
|
|
|
57
57
|
});
|
|
58
58
|
result.steps.extract.ok = true;
|
|
59
59
|
console.log("✅ [ATE Pipeline] Extract 완료");
|
|
60
|
-
} catch (err:
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
} catch (err: unknown) {
|
|
61
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
62
|
+
result.steps.extract.error = msg;
|
|
63
|
+
console.error(`❌ [ATE Pipeline] Extract 실패: ${msg}`);
|
|
63
64
|
return result;
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -72,9 +73,10 @@ export async function runFullPipeline(options: AutoPipelineOptions): Promise<Aut
|
|
|
72
73
|
});
|
|
73
74
|
result.steps.generate.ok = true;
|
|
74
75
|
console.log("✅ [ATE Pipeline] Generate 완료");
|
|
75
|
-
} catch (err:
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
} catch (err: unknown) {
|
|
77
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
78
|
+
result.steps.generate.error = msg;
|
|
79
|
+
console.error(`❌ [ATE Pipeline] Generate 실패: ${msg}`);
|
|
78
80
|
return result;
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -100,14 +102,15 @@ export async function runFullPipeline(options: AutoPipelineOptions): Promise<Aut
|
|
|
100
102
|
console.log(
|
|
101
103
|
`✅ [ATE Pipeline] Impact Analysis 완료 - ${impact.selectedRoutes.length}개 라우트 선택됨`,
|
|
102
104
|
);
|
|
103
|
-
} catch (err:
|
|
105
|
+
} catch (err: unknown) {
|
|
106
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
104
107
|
result.steps.impact = {
|
|
105
108
|
ok: false,
|
|
106
109
|
mode: "full",
|
|
107
110
|
selectedRoutes: [],
|
|
108
|
-
error:
|
|
111
|
+
error: msg,
|
|
109
112
|
};
|
|
110
|
-
console.warn(`⚠️ [ATE Pipeline] Impact Analysis 실패, 전체 테스트 실행: ${
|
|
113
|
+
console.warn(`⚠️ [ATE Pipeline] Impact Analysis 실패, 전체 테스트 실행: ${msg}`);
|
|
111
114
|
// Impact analysis 실패 시에도 계속 진행 (full test)
|
|
112
115
|
}
|
|
113
116
|
}
|
|
@@ -132,9 +135,10 @@ export async function runFullPipeline(options: AutoPipelineOptions): Promise<Aut
|
|
|
132
135
|
console.log(
|
|
133
136
|
`${exitCode === 0 ? "✅" : "⚠️"} [ATE Pipeline] Run 완료 - exitCode: ${exitCode}`,
|
|
134
137
|
);
|
|
135
|
-
} catch (err:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
} catch (err: unknown) {
|
|
139
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
140
|
+
result.steps.run.error = msg;
|
|
141
|
+
console.error(`❌ [ATE Pipeline] Run 실패: ${msg}`);
|
|
138
142
|
return result;
|
|
139
143
|
}
|
|
140
144
|
|
|
@@ -158,9 +162,10 @@ export async function runFullPipeline(options: AutoPipelineOptions): Promise<Aut
|
|
|
158
162
|
});
|
|
159
163
|
result.steps.report = { ok: true, summaryPath: reportResult.summaryPath };
|
|
160
164
|
console.log(`✅ [ATE Pipeline] Report 완료 - ${reportResult.summaryPath}`);
|
|
161
|
-
} catch (err:
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
} catch (err: unknown) {
|
|
166
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
167
|
+
result.steps.report.error = msg;
|
|
168
|
+
console.error(`❌ [ATE Pipeline] Report 실패: ${msg}`);
|
|
164
169
|
return result;
|
|
165
170
|
}
|
|
166
171
|
|
|
@@ -179,13 +184,14 @@ export async function runFullPipeline(options: AutoPipelineOptions): Promise<Aut
|
|
|
179
184
|
console.log(
|
|
180
185
|
`✅ [ATE Pipeline] Heal 완료 - ${healResult.suggestions?.length ?? 0}개 제안 생성됨`,
|
|
181
186
|
);
|
|
182
|
-
} catch (err:
|
|
187
|
+
} catch (err: unknown) {
|
|
188
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
183
189
|
result.steps.heal = {
|
|
184
190
|
ok: false,
|
|
185
191
|
suggestionsCount: 0,
|
|
186
|
-
error:
|
|
192
|
+
error: msg,
|
|
187
193
|
};
|
|
188
|
-
console.warn(`⚠️ [ATE Pipeline] Heal 실패: ${
|
|
194
|
+
console.warn(`⚠️ [ATE Pipeline] Heal 실패: ${msg}`);
|
|
189
195
|
// Heal 실패는 전체 파이프라인 실패로 보지 않음
|
|
190
196
|
}
|
|
191
197
|
}
|
|
@@ -197,9 +203,9 @@ export async function runFullPipeline(options: AutoPipelineOptions): Promise<Aut
|
|
|
197
203
|
);
|
|
198
204
|
|
|
199
205
|
return result;
|
|
200
|
-
} catch (err:
|
|
206
|
+
} catch (err: unknown) {
|
|
201
207
|
throw new ATEFileError(
|
|
202
|
-
`파이프라인 실행 중 예상치 못한 오류: ${err.message}`,
|
|
208
|
+
`파이프라인 실행 중 예상치 못한 오류: ${err instanceof Error ? err.message : String(err)}`,
|
|
203
209
|
"PIPELINE_ERROR",
|
|
204
210
|
options.repoRoot,
|
|
205
211
|
);
|
package/src/report.ts
CHANGED
|
@@ -27,8 +27,8 @@ export function composeSummary(params: {
|
|
|
27
27
|
let oracle;
|
|
28
28
|
try {
|
|
29
29
|
oracle = createDefaultOracle(params.oracleLevel);
|
|
30
|
-
} catch (err:
|
|
31
|
-
throw new Error(`Oracle 생성 실패: ${err.message}`);
|
|
30
|
+
} catch (err: unknown) {
|
|
31
|
+
throw new Error(`Oracle 생성 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
return {
|
|
@@ -74,16 +74,16 @@ export function writeSummary(repoRoot: string, runId: string, summary: SummaryJs
|
|
|
74
74
|
|
|
75
75
|
try {
|
|
76
76
|
ensureDir(runDir);
|
|
77
|
-
} catch (err:
|
|
78
|
-
throw new Error(`Report 디렉토리 생성 실패: ${err.message}`);
|
|
77
|
+
} catch (err: unknown) {
|
|
78
|
+
throw new Error(`Report 디렉토리 생성 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
const outPath = join(runDir, "summary.json");
|
|
82
82
|
|
|
83
83
|
try {
|
|
84
84
|
writeJson(outPath, summary);
|
|
85
|
-
} catch (err:
|
|
86
|
-
throw new Error(`Summary 파일 저장 실패: ${err.message}`);
|
|
85
|
+
} catch (err: unknown) {
|
|
86
|
+
throw new Error(`Summary 파일 저장 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
return outPath;
|
|
@@ -120,8 +120,8 @@ export async function generateReport(options: GenerateReportOptions): Promise<{
|
|
|
120
120
|
includeTraces,
|
|
121
121
|
});
|
|
122
122
|
result.html = htmlResult.path;
|
|
123
|
-
} catch (err:
|
|
124
|
-
throw new Error(`HTML 리포트 생성 실패: ${err.message}`);
|
|
123
|
+
} catch (err: unknown) {
|
|
124
|
+
throw new Error(`HTML 리포트 생성 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
package/src/reporter/html.ts
CHANGED
|
@@ -40,8 +40,8 @@ export async function generateHtmlReport(options: HtmlReportOptions): Promise<Ht
|
|
|
40
40
|
try {
|
|
41
41
|
const content = readFileSync(summaryPath, "utf-8");
|
|
42
42
|
summary = JSON.parse(content);
|
|
43
|
-
} catch (err:
|
|
44
|
-
throw new Error(`Summary 파일 읽기 실패: ${err.message}`);
|
|
43
|
+
} catch (err: unknown) {
|
|
44
|
+
throw new Error(`Summary 파일 읽기 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// 2. 스크린샷 URL 수집 (선택)
|
|
@@ -57,8 +57,8 @@ export async function generateHtmlReport(options: HtmlReportOptions): Promise<Ht
|
|
|
57
57
|
screenshotUrls.push(`./screenshots/${f}`);
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
|
-
} catch (err:
|
|
61
|
-
console.warn(`스크린샷 수집 실패: ${err.message}`);
|
|
60
|
+
} catch (err: unknown) {
|
|
61
|
+
console.warn(`스크린샷 수집 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -70,8 +70,8 @@ export async function generateHtmlReport(options: HtmlReportOptions): Promise<Ht
|
|
|
70
70
|
try {
|
|
71
71
|
ensureDir(join(htmlPath, "..")); // 상위 디렉토리 확인
|
|
72
72
|
writeFileSync(htmlPath, html, "utf-8");
|
|
73
|
-
} catch (err:
|
|
74
|
-
throw new Error(`HTML 파일 저장 실패: ${err.message}`);
|
|
73
|
+
} catch (err: unknown) {
|
|
74
|
+
throw new Error(`HTML 파일 저장 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
const size = Buffer.byteLength(html, "utf-8");
|
package/src/runner.ts
CHANGED
|
@@ -31,8 +31,8 @@ export async function runPlaywright(input: RunInput): Promise<RunResult> {
|
|
|
31
31
|
try {
|
|
32
32
|
ensureDir(runDir);
|
|
33
33
|
ensureDir(latestDir);
|
|
34
|
-
} catch (err:
|
|
35
|
-
throw new Error(`Report 디렉토리 생성 실패: ${err.message}`);
|
|
34
|
+
} catch (err: unknown) {
|
|
35
|
+
throw new Error(`Report 디렉토리 생성 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const baseURL = input.baseURL ?? process.env.BASE_URL ?? "http://localhost:3333";
|
|
@@ -57,8 +57,8 @@ export async function runPlaywright(input: RunInput): Promise<RunResult> {
|
|
|
57
57
|
stdio: "inherit",
|
|
58
58
|
env,
|
|
59
59
|
});
|
|
60
|
-
} catch (err:
|
|
61
|
-
throw new Error(`Playwright 프로세스 시작 실패: ${err.message}`);
|
|
60
|
+
} catch (err: unknown) {
|
|
61
|
+
throw new Error(`Playwright 프로세스 시작 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const exitCode: number = await new Promise((resolve, reject) => {
|
|
@@ -91,8 +91,8 @@ export async function runPlaywright(input: RunInput): Promise<RunResult> {
|
|
|
91
91
|
// record minimal run metadata
|
|
92
92
|
try {
|
|
93
93
|
writeJson(join(runDir, "run.json"), { ...result, baseURL, at: new Date().toISOString() });
|
|
94
|
-
} catch (err:
|
|
95
|
-
console.warn(`[ATE] Run metadata 저장 실패: ${err.message}`);
|
|
94
|
+
} catch (err: unknown) {
|
|
95
|
+
console.warn(`[ATE] Run metadata 저장 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
96
96
|
// Non-fatal: continue
|
|
97
97
|
}
|
|
98
98
|
|
package/src/scenario.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { getAtePaths, readJson, writeJson } from "./fs";
|
|
|
3
3
|
|
|
4
4
|
export interface GeneratedScenario {
|
|
5
5
|
id: string;
|
|
6
|
-
kind: "route-smoke";
|
|
6
|
+
kind: "route-smoke" | "api-smoke";
|
|
7
7
|
route: string;
|
|
8
|
+
methods?: string[];
|
|
8
9
|
oracleLevel: OracleLevel;
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -28,18 +29,22 @@ export function generateScenariosFromGraph(graph: InteractionGraph, oracleLevel:
|
|
|
28
29
|
throw new Error("빈 interaction graph입니다 (nodes가 없습니다)");
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const routes = graph.nodes.filter((n) => n.kind === "route") as Array<{ kind: "route"; id: string; path: string }>;
|
|
32
|
+
const routes = graph.nodes.filter((n) => n.kind === "route") as Array<{ kind: "route"; id: string; path: string; methods?: string[] }>;
|
|
32
33
|
|
|
33
34
|
if (routes.length === 0) {
|
|
34
35
|
console.warn("[ATE] 경고: route가 없습니다. 빈 시나리오 번들을 생성합니다.");
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
const scenarios: GeneratedScenario[] = routes.map((r) =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
const scenarios: GeneratedScenario[] = routes.map((r) => {
|
|
39
|
+
const isApi = r.path.startsWith("/api/") || (r.methods && r.methods.length > 0);
|
|
40
|
+
return {
|
|
41
|
+
id: `${isApi ? "api" : "route"}:${r.id}`,
|
|
42
|
+
kind: isApi ? "api-smoke" : "route-smoke",
|
|
43
|
+
route: r.id,
|
|
44
|
+
...(isApi && r.methods ? { methods: r.methods } : {}),
|
|
45
|
+
oracleLevel,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
43
48
|
|
|
44
49
|
return {
|
|
45
50
|
schemaVersion: 1,
|
|
@@ -55,16 +60,16 @@ export function generateAndWriteScenarios(repoRoot: string, oracleLevel: OracleL
|
|
|
55
60
|
let graph: InteractionGraph;
|
|
56
61
|
try {
|
|
57
62
|
graph = readJson<InteractionGraph>(paths.interactionGraphPath);
|
|
58
|
-
} catch (err:
|
|
59
|
-
throw new Error(`Interaction graph 읽기 실패: ${err.message}`);
|
|
63
|
+
} catch (err: unknown) {
|
|
64
|
+
throw new Error(`Interaction graph 읽기 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
const bundle = generateScenariosFromGraph(graph, oracleLevel);
|
|
63
68
|
|
|
64
69
|
try {
|
|
65
70
|
writeJson(paths.scenariosPath, bundle);
|
|
66
|
-
} catch (err:
|
|
67
|
-
throw new Error(`시나리오 파일 저장 실패: ${err.message}`);
|
|
71
|
+
} catch (err: unknown) {
|
|
72
|
+
throw new Error(`시나리오 파일 저장 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
return { scenariosPath: paths.scenariosPath, count: bundle.scenarios.length };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight type interfaces for ts-morph (lazily imported).
|
|
3
|
+
* These mirror the subset of ts-morph API used by ATE,
|
|
4
|
+
* avoiding a hard compile-time dependency on ts-morph.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Minimal ts-morph Node interface */
|
|
8
|
+
export interface Node {
|
|
9
|
+
getKind(): number;
|
|
10
|
+
getText(): string;
|
|
11
|
+
getDescendantsOfKind(kind: number): Node[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** ts-morph JsxAttribute */
|
|
15
|
+
export interface JsxAttribute extends Node {
|
|
16
|
+
getNameNode?(): { getText(): string };
|
|
17
|
+
getName?(): string;
|
|
18
|
+
getInitializer?(): (Node & { getLiteralValue?(): string }) | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** ts-morph CallExpression */
|
|
22
|
+
export interface CallExpression extends Node {
|
|
23
|
+
getExpression(): Node;
|
|
24
|
+
getArguments(): (Node & { getLiteralValue?(): string })[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** ts-morph SourceFile */
|
|
28
|
+
export interface SourceFile extends Node {
|
|
29
|
+
getFilePath(): string;
|
|
30
|
+
getExportedDeclarations(): ReadonlyMap<string, Node[]>;
|
|
31
|
+
getDescendantsOfKind(kind: number): Node[];
|
|
32
|
+
getImportDeclarations(): ImportDeclaration[];
|
|
33
|
+
getExportDeclarations(): ExportDeclaration[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** ts-morph ImportDeclaration */
|
|
37
|
+
export interface ImportDeclaration extends Node {
|
|
38
|
+
getModuleSpecifierValue(): string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** ts-morph ExportDeclaration */
|
|
42
|
+
export interface ExportDeclaration extends Node {
|
|
43
|
+
getModuleSpecifier(): (Node & { getLiteralText(): string }) | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** ts-morph Project */
|
|
47
|
+
export interface Project {
|
|
48
|
+
addSourceFileAtPath(filePath: string): SourceFile;
|
|
49
|
+
addSourceFilesAtPaths(globs: string[]): SourceFile[];
|
|
50
|
+
getSourceFile(filePath: string): SourceFile | undefined;
|
|
51
|
+
getSourceFiles(): SourceFile[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** SyntaxKind enum-like object used via dynamic import */
|
|
55
|
+
export interface SyntaxKindEnum {
|
|
56
|
+
StringLiteral: number;
|
|
57
|
+
JsxAttribute: number;
|
|
58
|
+
CallExpression: number;
|
|
59
|
+
[key: string]: string | number;
|
|
60
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface InteractionGraph {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export type InteractionNode =
|
|
39
|
-
| { kind: "route"; id: string; file: string; path: string }
|
|
39
|
+
| { kind: "route"; id: string; file: string; path: string; methods?: string[] }
|
|
40
40
|
| { kind: "modal"; id: string; file: string; name: string }
|
|
41
41
|
| { kind: "action"; id: string; file: string; name: string };
|
|
42
42
|
|