@elench/testkit 0.1.60 → 0.1.61
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/lib/config/database.mjs +53 -0
- package/lib/config/database.test.mjs +29 -0
- package/lib/config/discovery-config.mjs +13 -0
- package/lib/config/env.mjs +55 -0
- package/lib/config/env.test.mjs +40 -0
- package/lib/config/index.mjs +21 -807
- package/lib/config/paths.mjs +28 -0
- package/lib/config/paths.test.mjs +27 -0
- package/lib/config/runtime.mjs +241 -0
- package/lib/config/runtime.test.mjs +56 -0
- package/lib/config/skip-config.mjs +189 -0
- package/lib/config/skip-config.test.mjs +63 -0
- package/lib/config/telemetry.mjs +28 -0
- package/lib/config/validation.mjs +124 -0
- package/lib/coverage/backend-discovery.mjs +183 -0
- package/lib/coverage/backend-discovery.test.mjs +52 -0
- package/lib/coverage/evidence.mjs +146 -0
- package/lib/coverage/evidence.test.mjs +64 -0
- package/lib/coverage/fs-walk.mjs +64 -0
- package/lib/coverage/graph-builder.mjs +167 -0
- package/lib/coverage/index.mjs +1 -816
- package/lib/coverage/index.test.mjs +162 -14
- package/lib/coverage/next-discovery.mjs +174 -0
- package/lib/coverage/next-static-analysis.mjs +728 -0
- package/lib/coverage/routing.mjs +86 -0
- package/lib/coverage/routing.test.mjs +52 -0
- package/lib/coverage/shared.mjs +197 -0
- package/lib/coverage/shared.test.mjs +39 -0
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-bridge/src/index.mjs +82 -13
- package/node_modules/@elench/testkit-bridge/src/index.test.mjs +30 -5
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/package.json +5 -4
|
@@ -13,7 +13,7 @@ afterEach(() => {
|
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
describe("coverage graph builder", () => {
|
|
16
|
-
it("builds a Next page -> request -> API route ->
|
|
16
|
+
it("builds a Next page -> surface -> action -> request -> API route -> backend/data capability graph", () => {
|
|
17
17
|
const productDir = createProduct();
|
|
18
18
|
writeFile(
|
|
19
19
|
productDir,
|
|
@@ -38,15 +38,35 @@ describe("coverage graph builder", () => {
|
|
|
38
38
|
"src/app/campaigns/page.tsx",
|
|
39
39
|
`
|
|
40
40
|
"use client";
|
|
41
|
-
import { getJson } from "@/client/http/http";
|
|
41
|
+
import { getJson, postJson } from "@/client/http/http";
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
async function refreshCampaigns() {
|
|
44
44
|
await getJson("/api/campaigns");
|
|
45
|
-
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function createCampaign() {
|
|
48
|
+
await postJson("/api/campaigns");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function CampaignsPage() {
|
|
52
|
+
return (
|
|
53
|
+
<main>
|
|
54
|
+
<button data-testid="campaign-refresh-button" onClick={() => refreshCampaigns()}>
|
|
55
|
+
Refresh campaigns
|
|
56
|
+
</button>
|
|
57
|
+
<button data-testid="campaign-create-button" onClick={createCampaign}>
|
|
58
|
+
Create campaign
|
|
59
|
+
</button>
|
|
60
|
+
</main>
|
|
61
|
+
);
|
|
46
62
|
}
|
|
47
63
|
`
|
|
48
64
|
);
|
|
49
|
-
writeFile(
|
|
65
|
+
writeFile(
|
|
66
|
+
productDir,
|
|
67
|
+
"src/client/http/http.ts",
|
|
68
|
+
`export function getJson() { return null; }\nexport function postJson() { return null; }`
|
|
69
|
+
);
|
|
50
70
|
writeFile(
|
|
51
71
|
productDir,
|
|
52
72
|
"src/app/api/campaigns/route.ts",
|
|
@@ -62,7 +82,26 @@ describe("coverage graph builder", () => {
|
|
|
62
82
|
}
|
|
63
83
|
`
|
|
64
84
|
);
|
|
65
|
-
writeFile(
|
|
85
|
+
writeFile(
|
|
86
|
+
productDir,
|
|
87
|
+
"src/backend/server/campaigns/index.ts",
|
|
88
|
+
`
|
|
89
|
+
import { insertCampaignRow, selectCampaignRows } from "@/backend/data/campaigns";
|
|
90
|
+
|
|
91
|
+
export async function listCampaigns() {
|
|
92
|
+
return selectCampaignRows();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function createCampaign() {
|
|
96
|
+
return insertCampaignRow();
|
|
97
|
+
}
|
|
98
|
+
`
|
|
99
|
+
);
|
|
100
|
+
writeFile(
|
|
101
|
+
productDir,
|
|
102
|
+
"src/backend/data/campaigns/index.ts",
|
|
103
|
+
`export async function selectCampaignRows() {}\nexport async function insertCampaignRow() {}`
|
|
104
|
+
);
|
|
66
105
|
|
|
67
106
|
const context = buildServiceCoverageContext(productDir, "web", {
|
|
68
107
|
local: { cwd: ".", start: "node server.js", baseUrl: "http://127.0.0.1:3000", readyUrl: "http://127.0.0.1:3000" },
|
|
@@ -73,6 +112,10 @@ describe("coverage graph builder", () => {
|
|
|
73
112
|
route: "/campaigns",
|
|
74
113
|
filePath: "src/app/campaigns/page.tsx",
|
|
75
114
|
});
|
|
115
|
+
expect(context.pageByRoute.get("/campaigns").surfacesByTargetValue.get("campaign-refresh-button")).toMatchObject({
|
|
116
|
+
kind: "ui_surface",
|
|
117
|
+
label: "Refresh campaigns",
|
|
118
|
+
});
|
|
76
119
|
expect(context.apiRouteByKey.get("GET:/api/campaigns").node).toMatchObject({
|
|
77
120
|
kind: "api_route",
|
|
78
121
|
route: "/campaigns",
|
|
@@ -83,8 +126,18 @@ describe("coverage graph builder", () => {
|
|
|
83
126
|
expect(context.graph.edges).toEqual(
|
|
84
127
|
expect.arrayContaining([
|
|
85
128
|
expect.objectContaining({
|
|
86
|
-
kind: "
|
|
129
|
+
kind: "contains",
|
|
87
130
|
from: "page_view:web:/campaigns",
|
|
131
|
+
to: "ui_surface:web:/campaigns:campaign-refresh-button",
|
|
132
|
+
}),
|
|
133
|
+
expect.objectContaining({
|
|
134
|
+
kind: "triggers",
|
|
135
|
+
from: "ui_surface:web:/campaigns:campaign-refresh-button",
|
|
136
|
+
to: expect.stringContaining("ui_action:web:/campaigns"),
|
|
137
|
+
}),
|
|
138
|
+
expect.objectContaining({
|
|
139
|
+
kind: "requests",
|
|
140
|
+
from: expect.stringContaining("ui_action:web:/campaigns"),
|
|
88
141
|
}),
|
|
89
142
|
expect.objectContaining({
|
|
90
143
|
kind: "handles",
|
|
@@ -94,6 +147,11 @@ describe("coverage graph builder", () => {
|
|
|
94
147
|
kind: "delegates_to",
|
|
95
148
|
from: "api_route:web:GET:/api/campaigns",
|
|
96
149
|
}),
|
|
150
|
+
expect.objectContaining({
|
|
151
|
+
kind: "delegates_to",
|
|
152
|
+
from: "server_capability:src/backend/server/campaigns#createCampaign",
|
|
153
|
+
to: "data_capability:src/backend/data/campaigns#insertCampaignRow",
|
|
154
|
+
}),
|
|
97
155
|
])
|
|
98
156
|
);
|
|
99
157
|
});
|
|
@@ -107,8 +165,13 @@ describe("coverage graph builder", () => {
|
|
|
107
165
|
import { saveSettings } from "./actions";
|
|
108
166
|
|
|
109
167
|
export default function SettingsPage() {
|
|
110
|
-
|
|
111
|
-
|
|
168
|
+
return (
|
|
169
|
+
<form action={saveSettings}>
|
|
170
|
+
<button data-testid="settings-save-button" type="submit">
|
|
171
|
+
Save settings
|
|
172
|
+
</button>
|
|
173
|
+
</form>
|
|
174
|
+
);
|
|
112
175
|
}
|
|
113
176
|
`
|
|
114
177
|
);
|
|
@@ -138,7 +201,12 @@ describe("coverage graph builder", () => {
|
|
|
138
201
|
expect.arrayContaining([
|
|
139
202
|
expect.objectContaining({
|
|
140
203
|
kind: "triggers",
|
|
141
|
-
from: "
|
|
204
|
+
from: expect.stringContaining("ui_surface:web:/settings"),
|
|
205
|
+
to: "ui_action:web:/settings:saveSettings",
|
|
206
|
+
}),
|
|
207
|
+
expect.objectContaining({
|
|
208
|
+
kind: "triggers",
|
|
209
|
+
from: "ui_action:web:/settings:saveSettings",
|
|
142
210
|
to: "server_action:web:src/app/settings/actions.ts#saveSettings",
|
|
143
211
|
}),
|
|
144
212
|
expect.objectContaining({
|
|
@@ -149,10 +217,48 @@ describe("coverage graph builder", () => {
|
|
|
149
217
|
);
|
|
150
218
|
});
|
|
151
219
|
|
|
152
|
-
it("attaches convention-based test evidence to page
|
|
220
|
+
it("attaches convention-based test evidence to page, surface, route, and data nodes", () => {
|
|
153
221
|
const productDir = createProduct();
|
|
154
|
-
writeFile(
|
|
155
|
-
|
|
222
|
+
writeFile(
|
|
223
|
+
productDir,
|
|
224
|
+
"src/app/campaigns/page.tsx",
|
|
225
|
+
`
|
|
226
|
+
"use client";
|
|
227
|
+
import { getJson } from "@/client/http/http";
|
|
228
|
+
|
|
229
|
+
async function loadCampaigns() {
|
|
230
|
+
await getJson("/api/campaigns");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export default function CampaignsPage() {
|
|
234
|
+
return <button data-testid="campaign-save-button" onClick={loadCampaigns}>Save campaign</button>;
|
|
235
|
+
}
|
|
236
|
+
`
|
|
237
|
+
);
|
|
238
|
+
writeFile(productDir, "src/client/http/http.ts", `export async function getJson() { return null; }`);
|
|
239
|
+
writeFile(
|
|
240
|
+
productDir,
|
|
241
|
+
"src/app/api/campaigns/route.ts",
|
|
242
|
+
`
|
|
243
|
+
import { listCampaigns } from "@/backend/server/campaigns";
|
|
244
|
+
|
|
245
|
+
export async function GET() {
|
|
246
|
+
return listCampaigns();
|
|
247
|
+
}
|
|
248
|
+
`
|
|
249
|
+
);
|
|
250
|
+
writeFile(
|
|
251
|
+
productDir,
|
|
252
|
+
"src/backend/server/campaigns/index.ts",
|
|
253
|
+
`
|
|
254
|
+
import { saveCampaignRow } from "@/backend/data/campaigns";
|
|
255
|
+
|
|
256
|
+
export async function listCampaigns() {
|
|
257
|
+
return saveCampaignRow();
|
|
258
|
+
}
|
|
259
|
+
`
|
|
260
|
+
);
|
|
261
|
+
writeFile(productDir, "src/backend/data/campaigns/index.ts", `export async function saveCampaignRow() {}`);
|
|
156
262
|
writeFile(
|
|
157
263
|
productDir,
|
|
158
264
|
"src/app/campaigns/__testkit__/campaigns.pw.testkit.ts",
|
|
@@ -165,6 +271,32 @@ describe("coverage graph builder", () => {
|
|
|
165
271
|
});
|
|
166
272
|
`
|
|
167
273
|
);
|
|
274
|
+
writeFile(
|
|
275
|
+
productDir,
|
|
276
|
+
"src/app/api/campaigns/__testkit__/campaigns.int.testkit.ts",
|
|
277
|
+
`
|
|
278
|
+
import { defineHttpSuite } from "@elench/testkit";
|
|
279
|
+
|
|
280
|
+
const suite = defineHttpSuite(({ rawReq }) => {
|
|
281
|
+
rawReq("GET", "/api/campaigns");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
export default suite;
|
|
285
|
+
`
|
|
286
|
+
);
|
|
287
|
+
writeFile(
|
|
288
|
+
productDir,
|
|
289
|
+
"src/backend/data/campaigns/__testkit__/campaigns.dal.testkit.ts",
|
|
290
|
+
`
|
|
291
|
+
import { defineDalSuite } from "@elench/testkit";
|
|
292
|
+
|
|
293
|
+
const suite = defineDalSuite(({ db }) => {
|
|
294
|
+
db.query("SELECT 1 AS value");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
export default suite;
|
|
298
|
+
`
|
|
299
|
+
);
|
|
168
300
|
|
|
169
301
|
const graph = buildCoverageGraph({
|
|
170
302
|
productDir,
|
|
@@ -188,6 +320,13 @@ describe("coverage graph builder", () => {
|
|
|
188
320
|
suiteName: "campaigns",
|
|
189
321
|
filePath: "src/app/api/campaigns/__testkit__/campaigns.int.testkit.ts",
|
|
190
322
|
},
|
|
323
|
+
{
|
|
324
|
+
serviceName: "web",
|
|
325
|
+
type: "dal",
|
|
326
|
+
framework: "k6",
|
|
327
|
+
suiteName: "campaigns",
|
|
328
|
+
filePath: "src/backend/data/campaigns/__testkit__/campaigns.dal.testkit.ts",
|
|
329
|
+
},
|
|
191
330
|
],
|
|
192
331
|
});
|
|
193
332
|
|
|
@@ -195,7 +334,10 @@ describe("coverage graph builder", () => {
|
|
|
195
334
|
expect.arrayContaining([
|
|
196
335
|
expect.objectContaining({
|
|
197
336
|
testFilePath: "src/app/campaigns/__testkit__/campaigns.pw.testkit.ts",
|
|
198
|
-
coveredNodeIds: [
|
|
337
|
+
coveredNodeIds: expect.arrayContaining([
|
|
338
|
+
"page_view:web:/campaigns",
|
|
339
|
+
"ui_surface:web:/campaigns:campaign-save-button",
|
|
340
|
+
]),
|
|
199
341
|
details: expect.objectContaining({
|
|
200
342
|
route: "/campaigns",
|
|
201
343
|
targets: [
|
|
@@ -210,6 +352,12 @@ describe("coverage graph builder", () => {
|
|
|
210
352
|
testFilePath: "src/app/api/campaigns/__testkit__/campaigns.int.testkit.ts",
|
|
211
353
|
coveredNodeIds: ["api_route:web:GET:/api/campaigns"],
|
|
212
354
|
}),
|
|
355
|
+
expect.objectContaining({
|
|
356
|
+
testFilePath: "src/backend/data/campaigns/__testkit__/campaigns.dal.testkit.ts",
|
|
357
|
+
coveredNodeIds: expect.arrayContaining([
|
|
358
|
+
expect.stringContaining("data_capability:src/backend/data/campaigns"),
|
|
359
|
+
]),
|
|
360
|
+
}),
|
|
213
361
|
])
|
|
214
362
|
);
|
|
215
363
|
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { analyzeNextPageFile } from "./next-static-analysis.mjs";
|
|
4
|
+
import { extractBackendImports, extractExportedFunctions, extractExportedMethodBodies } from "./backend-discovery.mjs";
|
|
5
|
+
import { resolveImportToSourceFile, walkFiles } from "./fs-walk.mjs";
|
|
6
|
+
import { routeFromApiFile, routeFromAppFile } from "./routing.mjs";
|
|
7
|
+
import {
|
|
8
|
+
dedupeNodes,
|
|
9
|
+
hasWord,
|
|
10
|
+
HTTP_METHODS,
|
|
11
|
+
isServerActionFile,
|
|
12
|
+
normalizePath,
|
|
13
|
+
normalizeRoute,
|
|
14
|
+
pageLabelFromRoute,
|
|
15
|
+
toApiRequestPath,
|
|
16
|
+
} from "./shared.mjs";
|
|
17
|
+
|
|
18
|
+
export function discoverPageViews({ serviceName, serviceRoot, nextAppRoot, exclude = [], resolveImportToSourceFile }) {
|
|
19
|
+
const pageFiles = walkFiles(nextAppRoot, { baseDir: serviceRoot, exclude }).filter(
|
|
20
|
+
(filePath) => filePath.endsWith("/page.tsx") || filePath.endsWith("/page.ts")
|
|
21
|
+
);
|
|
22
|
+
const nodes = [];
|
|
23
|
+
const edges = [];
|
|
24
|
+
const pageEntries = [];
|
|
25
|
+
|
|
26
|
+
for (const absolutePath of pageFiles) {
|
|
27
|
+
const relativePath = normalizePath(path.relative(serviceRoot, absolutePath));
|
|
28
|
+
const route = routeFromAppFile(nextAppRoot, absolutePath);
|
|
29
|
+
const content = fs.readFileSync(absolutePath, "utf8");
|
|
30
|
+
const node = {
|
|
31
|
+
id: `page_view:${serviceName}:${route}`,
|
|
32
|
+
kind: "page_view",
|
|
33
|
+
service: serviceName,
|
|
34
|
+
label: pageLabelFromRoute(route),
|
|
35
|
+
route,
|
|
36
|
+
filePath: relativePath,
|
|
37
|
+
};
|
|
38
|
+
nodes.push(node);
|
|
39
|
+
|
|
40
|
+
const pageAnalysis = analyzeNextPageFile({
|
|
41
|
+
serviceName,
|
|
42
|
+
serviceRoot,
|
|
43
|
+
filePath: relativePath,
|
|
44
|
+
route,
|
|
45
|
+
content,
|
|
46
|
+
readSourceFile: (resolvedPath) => {
|
|
47
|
+
const absoluteResolved = path.join(serviceRoot, resolvedPath);
|
|
48
|
+
return fs.existsSync(absoluteResolved) ? fs.readFileSync(absoluteResolved, "utf8") : null;
|
|
49
|
+
},
|
|
50
|
+
resolveImportPath: (specifier) => resolveImportToSourceFile(serviceRoot, relativePath, specifier),
|
|
51
|
+
isServerActionFile,
|
|
52
|
+
normalizeRoute,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
for (const analysisNode of pageAnalysis.nodes) nodes.push(analysisNode);
|
|
56
|
+
for (const analysisEdge of pageAnalysis.edges) edges.push(analysisEdge);
|
|
57
|
+
|
|
58
|
+
pageEntries.push({
|
|
59
|
+
node,
|
|
60
|
+
route,
|
|
61
|
+
filePath: relativePath,
|
|
62
|
+
requests: pageAnalysis.requests,
|
|
63
|
+
serverActionRefs: pageAnalysis.serverActionRefs,
|
|
64
|
+
surfacesByTargetValue: pageAnalysis.surfacesByTargetValue,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { nodes, edges, pageEntries };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function discoverApiRoutes({ serviceName, serviceRoot, nextAppRoot, exclude = [], resolveImportToSourceFile }) {
|
|
72
|
+
const routeFiles = walkFiles(path.join(nextAppRoot, "api"), { baseDir: serviceRoot, exclude }).filter(
|
|
73
|
+
(filePath) => filePath.endsWith("/route.ts") || filePath.endsWith("/route.tsx") || filePath.endsWith("/route.js")
|
|
74
|
+
);
|
|
75
|
+
const nodes = [];
|
|
76
|
+
const edges = [];
|
|
77
|
+
const routeEntries = [];
|
|
78
|
+
|
|
79
|
+
for (const absolutePath of routeFiles) {
|
|
80
|
+
const filePath = normalizePath(path.relative(serviceRoot, absolutePath));
|
|
81
|
+
const route = routeFromApiFile(nextAppRoot, absolutePath);
|
|
82
|
+
const requestPath = toApiRequestPath(route);
|
|
83
|
+
const content = fs.readFileSync(absolutePath, "utf8");
|
|
84
|
+
const methodBodies = extractExportedMethodBodies(content, HTTP_METHODS);
|
|
85
|
+
const backendImports = extractBackendImports({ serviceName, serviceRoot, filePath, content, resolveImportToSourceFile });
|
|
86
|
+
|
|
87
|
+
for (const [method, body] of methodBodies) {
|
|
88
|
+
const node = {
|
|
89
|
+
id: `api_route:${serviceName}:${method}:${requestPath}`,
|
|
90
|
+
kind: "api_route",
|
|
91
|
+
service: serviceName,
|
|
92
|
+
label: `${method} ${requestPath}`,
|
|
93
|
+
route,
|
|
94
|
+
method,
|
|
95
|
+
path: requestPath,
|
|
96
|
+
filePath,
|
|
97
|
+
};
|
|
98
|
+
nodes.push(node);
|
|
99
|
+
|
|
100
|
+
const matchedCapabilities = backendImports.filter((entry) => hasWord(body, entry.importName));
|
|
101
|
+
for (const capability of matchedCapabilities) {
|
|
102
|
+
nodes.push(capability.node);
|
|
103
|
+
edges.push({
|
|
104
|
+
id: `delegates_to:${node.id}:${capability.node.id}`,
|
|
105
|
+
kind: "delegates_to",
|
|
106
|
+
from: node.id,
|
|
107
|
+
to: capability.node.id,
|
|
108
|
+
confidence: "high",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
routeEntries.push({ node, method, route, requestPath, filePath });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { nodes: dedupeNodes(nodes), edges, routeEntries };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function discoverServerActions({ serviceName, serviceRoot, exclude = [], resolveImportToSourceFile }) {
|
|
120
|
+
const appRoots = [path.join(serviceRoot, "app"), path.join(serviceRoot, "src", "app")].filter((candidate) =>
|
|
121
|
+
fs.existsSync(candidate)
|
|
122
|
+
);
|
|
123
|
+
const nodes = [];
|
|
124
|
+
const edges = [];
|
|
125
|
+
const actionEntries = [];
|
|
126
|
+
|
|
127
|
+
for (const appRoot of appRoots) {
|
|
128
|
+
for (const absolutePath of walkFiles(appRoot, { baseDir: serviceRoot, exclude })) {
|
|
129
|
+
if (!absolutePath.endsWith(".ts") && !absolutePath.endsWith(".tsx")) continue;
|
|
130
|
+
const content = fs.readFileSync(absolutePath, "utf8");
|
|
131
|
+
if (!isServerActionFile(content)) continue;
|
|
132
|
+
|
|
133
|
+
const relativePath = normalizePath(path.relative(serviceRoot, absolutePath));
|
|
134
|
+
const backendImports = extractBackendImports({
|
|
135
|
+
serviceName,
|
|
136
|
+
serviceRoot,
|
|
137
|
+
filePath: relativePath,
|
|
138
|
+
content,
|
|
139
|
+
resolveImportToSourceFile,
|
|
140
|
+
});
|
|
141
|
+
const exportedFunctions = extractExportedFunctions(content);
|
|
142
|
+
|
|
143
|
+
for (const exported of exportedFunctions) {
|
|
144
|
+
const node = {
|
|
145
|
+
id: `server_action:${serviceName}:${relativePath}#${exported.name}`,
|
|
146
|
+
kind: "server_action",
|
|
147
|
+
service: serviceName,
|
|
148
|
+
label: exported.name,
|
|
149
|
+
filePath: relativePath,
|
|
150
|
+
};
|
|
151
|
+
nodes.push(node);
|
|
152
|
+
actionEntries.push({
|
|
153
|
+
node,
|
|
154
|
+
exportName: exported.name,
|
|
155
|
+
sourceFile: relativePath,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const matchedCapabilities = backendImports.filter((entry) => hasWord(exported.body, entry.importName));
|
|
159
|
+
for (const capability of matchedCapabilities) {
|
|
160
|
+
nodes.push(capability.node);
|
|
161
|
+
edges.push({
|
|
162
|
+
id: `delegates_to:${node.id}:${capability.node.id}`,
|
|
163
|
+
kind: "delegates_to",
|
|
164
|
+
from: node.id,
|
|
165
|
+
to: capability.node.id,
|
|
166
|
+
confidence: "high",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { nodes: dedupeNodes(nodes), edges, actionEntries };
|
|
174
|
+
}
|