(ctx: ResolutionContext, onYield: MaybeYield)
| 2469 | } |
| 2470 | |
| 2471 | async function vuexDispatchEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> { |
| 2472 | let scannedFiles = 0; |
| 2473 | const storeFileCache = new Map<string, boolean>(); |
| 2474 | const isStoreFile = (file: string): boolean => { |
| 2475 | let v = storeFileCache.get(file); |
| 2476 | if (v === undefined) { |
| 2477 | const c = ctx.readFile(file); |
| 2478 | const seen = new Set<string>(); |
| 2479 | if (c) { |
| 2480 | VUEX_STORE_SIGNAL.lastIndex = 0; |
| 2481 | let sm: RegExpExecArray | null; |
| 2482 | while ((sm = VUEX_STORE_SIGNAL.exec(c))) { seen.add(sm[0]); if (seen.size >= 2) break; } |
| 2483 | } |
| 2484 | v = seen.size >= 2; |
| 2485 | storeFileCache.set(file, v); |
| 2486 | } |
| 2487 | return v; |
| 2488 | }; |
| 2489 | |
| 2490 | const resolve = (key: string, dispatchFile: string): Node | null => { |
| 2491 | const segs = key.split('/'); |
| 2492 | const action = segs[segs.length - 1]!; |
| 2493 | const cands = ctx.getNodesByName(action).filter((n) => n.kind === 'function' && isStoreFile(n.filePath)); |
| 2494 | if (!cands.length) return null; |
| 2495 | if (segs.length > 1) { |
| 2496 | const mod = segs[segs.length - 2]!; // immediate namespace ≈ the module file |
| 2497 | return cands.find((c) => pathHasSegment(c.filePath, mod)) ?? (cands.length === 1 ? cands[0]! : null); |
| 2498 | } |
| 2499 | // Root key: a local `commit('M')` inside an action targets the same module file; |
| 2500 | // otherwise accept only an unambiguous single store-wide match. |
| 2501 | return cands.find((c) => c.filePath === dispatchFile) ?? (cands.length === 1 ? cands[0]! : null); |
| 2502 | }; |
| 2503 | |
| 2504 | const edges: Edge[] = []; |
| 2505 | const seen = new Set<string>(); |
| 2506 | for (const file of ctx.getAllFiles()) { |
| 2507 | if ((++scannedFiles & 15) === 0) await onYield(); |
| 2508 | if (!PINIA_CONSUMER_EXT.test(file)) continue; |
| 2509 | const content = ctx.readFile(file); |
| 2510 | if (!content || (!content.includes('dispatch(') && !content.includes('commit('))) continue; |
| 2511 | const safe = stripCommentsForRegex(content, /\.(?:jsx?|mjs|cjs)$/.test(file) ? 'javascript' : 'typescript'); |
| 2512 | const nodesInFile = ctx.getNodesInFile(file); |
| 2513 | const fallback = nodesInFile.find((n) => n.kind === 'component'); // .vue top-level |
| 2514 | VUEX_DISPATCH_RE.lastIndex = 0; |
| 2515 | let m: RegExpExecArray | null; |
| 2516 | let added = 0; |
| 2517 | while ((m = VUEX_DISPATCH_RE.exec(safe)) && added < VUEX_FANOUT_CAP) { |
| 2518 | const key = m[1]!; |
| 2519 | const line = safe.slice(0, m.index).split('\n').length; |
| 2520 | const disp = enclosingFn(nodesInFile, line) ?? fallback; |
| 2521 | if (!disp) continue; |
| 2522 | const target = resolve(key, file); |
| 2523 | if (!target || target.id === disp.id) continue; |
| 2524 | const edgeKey = `${disp.id}>${target.id}`; |
| 2525 | if (seen.has(edgeKey)) continue; |
| 2526 | seen.add(edgeKey); |
| 2527 | edges.push({ |
| 2528 | source: disp.id, |
no test coverage detected