@elench/testkit 0.1.59 → 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 -776
- package/lib/coverage/index.test.mjs +183 -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 +101 -15
- package/node_modules/@elench/testkit-bridge/src/index.test.mjs +36 -6
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/testkit-protocol/src/index.d.ts +1 -0
- package/node_modules/@elench/testkit-protocol/src/index.mjs +3 -1
- package/node_modules/@elench/testkit-protocol/src/index.test.mjs +14 -0
- 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,86 @@ 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() {}`);
|
|
262
|
+
writeFile(
|
|
263
|
+
productDir,
|
|
264
|
+
"src/app/campaigns/__testkit__/campaigns.pw.testkit.ts",
|
|
265
|
+
`
|
|
266
|
+
import { expect, test } from "@playwright/test";
|
|
267
|
+
|
|
268
|
+
test("campaigns route", async ({ page }) => {
|
|
269
|
+
await page.goto("/campaigns");
|
|
270
|
+
await expect(page.getByTestId("campaign-save-button")).toBeVisible();
|
|
271
|
+
});
|
|
272
|
+
`
|
|
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
|
+
);
|
|
156
300
|
|
|
157
301
|
const graph = buildCoverageGraph({
|
|
158
302
|
productDir,
|
|
@@ -176,6 +320,13 @@ describe("coverage graph builder", () => {
|
|
|
176
320
|
suiteName: "campaigns",
|
|
177
321
|
filePath: "src/app/api/campaigns/__testkit__/campaigns.int.testkit.ts",
|
|
178
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
|
+
},
|
|
179
330
|
],
|
|
180
331
|
});
|
|
181
332
|
|
|
@@ -183,12 +334,30 @@ describe("coverage graph builder", () => {
|
|
|
183
334
|
expect.arrayContaining([
|
|
184
335
|
expect.objectContaining({
|
|
185
336
|
testFilePath: "src/app/campaigns/__testkit__/campaigns.pw.testkit.ts",
|
|
186
|
-
coveredNodeIds: [
|
|
337
|
+
coveredNodeIds: expect.arrayContaining([
|
|
338
|
+
"page_view:web:/campaigns",
|
|
339
|
+
"ui_surface:web:/campaigns:campaign-save-button",
|
|
340
|
+
]),
|
|
341
|
+
details: expect.objectContaining({
|
|
342
|
+
route: "/campaigns",
|
|
343
|
+
targets: [
|
|
344
|
+
expect.objectContaining({
|
|
345
|
+
kind: "testId",
|
|
346
|
+
value: "campaign-save-button",
|
|
347
|
+
}),
|
|
348
|
+
],
|
|
349
|
+
}),
|
|
187
350
|
}),
|
|
188
351
|
expect.objectContaining({
|
|
189
352
|
testFilePath: "src/app/api/campaigns/__testkit__/campaigns.int.testkit.ts",
|
|
190
353
|
coveredNodeIds: ["api_route:web:GET:/api/campaigns"],
|
|
191
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
|
+
}),
|
|
192
361
|
])
|
|
193
362
|
);
|
|
194
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
|
+
}
|