@getmikk/diagram-generator 1.2.0 → 1.3.1

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/README.md ADDED
@@ -0,0 +1,245 @@
1
+ # @getmikk/diagram-generator
2
+
3
+ > Mermaid.js diagram generation — produces architecture, flow, health, impact, capsule, and dependency matrix visualizations from your codebase's lock file.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@getmikk/diagram-generator)](https://www.npmjs.com/package/@getmikk/diagram-generator)
6
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](../../LICENSE)
7
+
8
+ `@getmikk/diagram-generator` turns the `mikk.json` contract and `mikk.lock.json` lock file into rich [Mermaid.js](https://mermaid.js.org/) diagrams. Every diagram is generated entirely from AST-derived data — no manual drawing required. Supports 7 diagram types covering everything from high-level architecture to per-function call flows.
9
+
10
+ ---
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @getmikk/diagram-generator
16
+ # or
17
+ bun add @getmikk/diagram-generator
18
+ ```
19
+
20
+ **Peer dependency:** `@getmikk/core`
21
+
22
+ ---
23
+
24
+ ## Quick Start
25
+
26
+ ```typescript
27
+ import { DiagramOrchestrator } from '@getmikk/diagram-generator'
28
+ import { ContractReader, LockReader } from '@getmikk/core'
29
+
30
+ const contract = await new ContractReader().read('./mikk.json')
31
+ const lock = await new LockReader().read('./mikk.lock.json')
32
+
33
+ const orchestrator = new DiagramOrchestrator(contract, lock, process.cwd())
34
+ const result = await orchestrator.generateAll()
35
+
36
+ console.log(result.generated) // List of generated .mmd file paths
37
+ // Files written to .mikk/diagrams/
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Diagram Types
43
+
44
+ ### 1. Main Architecture Diagram
45
+
46
+ High-level `graph TD` showing all modules with file/function counts and inter-module edges.
47
+
48
+ ```typescript
49
+ import { MainDiagramGenerator } from '@getmikk/diagram-generator'
50
+
51
+ const gen = new MainDiagramGenerator(contract, lock)
52
+ const mermaid = gen.generate()
53
+ ```
54
+
55
+ **Output example:**
56
+
57
+ ```mermaid
58
+ graph TD
59
+ auth["auth<br/>📁 5 files · 📦 12 functions"]
60
+ payments["payments<br/>📁 3 files · 📦 8 functions"]
61
+ auth --> payments
62
+ ```
63
+
64
+ ---
65
+
66
+ ### 2. Module Detail Diagram
67
+
68
+ Zoomed-in per-module diagram showing file subgraphs, internal call edges, and external dependencies.
69
+
70
+ ```typescript
71
+ import { ModuleDiagramGenerator } from '@getmikk/diagram-generator'
72
+
73
+ const gen = new ModuleDiagramGenerator(contract, lock)
74
+ const mermaid = gen.generate('auth') // module ID
75
+ ```
76
+
77
+ Shows:
78
+ - Subgraphs for each file in the module
79
+ - Function nodes within each file
80
+ - Internal call edges between functions
81
+ - External call links to other modules
82
+
83
+ ---
84
+
85
+ ### 3. Impact Diagram
86
+
87
+ Visualizes the blast radius of changes — what's directly changed (red) vs. transitively impacted (orange).
88
+
89
+ ```typescript
90
+ import { ImpactDiagramGenerator } from '@getmikk/diagram-generator'
91
+
92
+ const gen = new ImpactDiagramGenerator(lock)
93
+ const mermaid = gen.generate(
94
+ ['auth/login.ts::validateToken'], // changed node IDs
95
+ ['payments/checkout.ts::processPayment'] // impacted node IDs
96
+ )
97
+ ```
98
+
99
+ **Output:** `graph LR` with color-coded nodes:
100
+ - 🔴 **Red** — directly changed
101
+ - 🟠 **Orange** — transitively impacted
102
+ - Edges show the propagation chain
103
+
104
+ ---
105
+
106
+ ### 4. Health Dashboard
107
+
108
+ Module health overview with cohesion percentage, coupling count, function count, and color-coded status.
109
+
110
+ ```typescript
111
+ import { HealthDiagramGenerator } from '@getmikk/diagram-generator'
112
+
113
+ const gen = new HealthDiagramGenerator(contract, lock)
114
+ const mermaid = gen.generate()
115
+ ```
116
+
117
+ **Metrics per module:**
118
+
119
+ | Metric | Description |
120
+ |--------|-------------|
121
+ | Cohesion % | Ratio of internal calls to total calls (higher = better) |
122
+ | Coupling | Count of cross-module dependencies (lower = better) |
123
+ | Functions | Total function count |
124
+ | Health | 🟢 Green (>70% cohesion) · 🟡 Yellow (40-70%) · 🔴 Red (<40%) |
125
+
126
+ ---
127
+
128
+ ### 5. Flow Diagram (Sequence)
129
+
130
+ Traces a function's call chain as a Mermaid sequence diagram.
131
+
132
+ ```typescript
133
+ import { FlowDiagramGenerator } from '@getmikk/diagram-generator'
134
+
135
+ const gen = new FlowDiagramGenerator(lock)
136
+
137
+ // Trace from a specific function
138
+ const sequence = gen.generate('auth/login.ts::handleLogin', /* maxDepth */ 5)
139
+
140
+ // Show all entry-point functions grouped by module
141
+ const entryPoints = gen.generateEntryPoints()
142
+ ```
143
+
144
+ The sequence diagram follows the call graph depth-first, showing which function calls which, across module boundaries.
145
+
146
+ ---
147
+
148
+ ### 6. Capsule Diagram
149
+
150
+ Shows a module's public API surface — the "capsule" boundary:
151
+
152
+ ```typescript
153
+ import { CapsuleDiagramGenerator } from '@getmikk/diagram-generator'
154
+
155
+ const gen = new CapsuleDiagramGenerator(contract, lock)
156
+ const mermaid = gen.generate('auth') // module ID
157
+ ```
158
+
159
+ **Visualizes:**
160
+ - **Public functions** — exported and listed in the module's `publicApi`
161
+ - **Internal functions** — everything else
162
+ - **External consumers** — other modules that call into this module's public API
163
+
164
+ ---
165
+
166
+ ### 7. Dependency Matrix
167
+
168
+ N×N cross-module dependency analysis:
169
+
170
+ ```typescript
171
+ import { DependencyMatrixGenerator } from '@getmikk/diagram-generator'
172
+
173
+ const gen = new DependencyMatrixGenerator(contract, lock)
174
+
175
+ // Mermaid graph with weighted edges
176
+ const graph = gen.generate()
177
+
178
+ // Markdown table (N×N matrix)
179
+ const table = gen.generateTable()
180
+ ```
181
+
182
+ **Markdown table example:**
183
+
184
+ | | auth | payments | users |
185
+ |---|---|---|---|
186
+ | **auth** | — | 5 | 2 |
187
+ | **payments** | 1 | — | 3 |
188
+ | **users** | 0 | 0 | — |
189
+
190
+ Numbers represent cross-module function call counts.
191
+
192
+ ---
193
+
194
+ ## DiagramOrchestrator
195
+
196
+ The orchestrator generates all diagrams at once and writes them to `.mikk/diagrams/`:
197
+
198
+ ```typescript
199
+ import { DiagramOrchestrator } from '@getmikk/diagram-generator'
200
+
201
+ const orchestrator = new DiagramOrchestrator(contract, lock, projectRoot)
202
+
203
+ // Generate everything
204
+ const result = await orchestrator.generateAll()
205
+ // Writes:
206
+ // .mikk/diagrams/main.mmd
207
+ // .mikk/diagrams/health.mmd
208
+ // .mikk/diagrams/matrix.mmd
209
+ // .mikk/diagrams/flow-entrypoints.mmd
210
+ // .mikk/diagrams/module-{id}.mmd (one per module)
211
+ // .mikk/diagrams/capsule-{id}.mmd (one per module)
212
+
213
+ // Generate impact diagram for specific changes
214
+ const impactMmd = await orchestrator.generateImpact(changedIds, impactedIds)
215
+ // Writes: .mikk/diagrams/impact.mmd
216
+ ```
217
+
218
+ **Output files:** All diagrams are `.mmd` files that can be rendered with:
219
+ - [Mermaid Live Editor](https://mermaid.live/)
220
+ - GitHub Markdown (native Mermaid support)
221
+ - VS Code Mermaid extensions
222
+ - `mmdc` CLI (mermaid-cli)
223
+
224
+ ---
225
+
226
+ ## Types
227
+
228
+ ```typescript
229
+ import {
230
+ DiagramOrchestrator,
231
+ MainDiagramGenerator,
232
+ ModuleDiagramGenerator,
233
+ ImpactDiagramGenerator,
234
+ HealthDiagramGenerator,
235
+ FlowDiagramGenerator,
236
+ CapsuleDiagramGenerator,
237
+ DependencyMatrixGenerator,
238
+ } from '@getmikk/diagram-generator'
239
+ ```
240
+
241
+ ---
242
+
243
+ ## License
244
+
245
+ [Apache-2.0](../../LICENSE)
package/package.json CHANGED
@@ -1,26 +1,30 @@
1
- {
2
- "name": "@getmikk/diagram-generator",
3
- "version": "1.2.0",
4
- "type": "module",
5
- "main": "./dist/index.js",
6
- "types": "./dist/index.d.ts",
7
- "exports": {
8
- ".": {
9
- "import": "./dist/index.js",
10
- "types": "./dist/index.d.ts"
11
- }
12
- },
13
- "scripts": {
14
- "build": "tsc",
15
- "test": "bun test",
16
- "publish": "npm publish --access public",
17
- "dev": "tsc --watch"
18
- },
19
- "dependencies": {
20
- "@getmikk/core": "workspace:*"
21
- },
22
- "devDependencies": {
23
- "typescript": "^5.7.0",
24
- "@types/node": "^22.0.0"
25
- }
26
- }
1
+ {
2
+ "name": "@getmikk/diagram-generator",
3
+ "version": "1.3.1",
4
+ "license": "Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/Ansh-dhanani/mikk"
8
+ },
9
+ "type": "module",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "test": "bun test",
21
+ "dev": "tsc --watch"
22
+ },
23
+ "dependencies": {
24
+ "@getmikk/core": "^1.3.1"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "^5.7.0",
28
+ "@types/node": "^22.0.0"
29
+ }
30
+ }
@@ -1,79 +1,79 @@
1
- import type { MikkContract, MikkLock } from '@getmikk/core'
2
-
3
- /**
4
- * CapsuleDiagramGenerator — generates capsule diagrams that show
5
- * the public API surface of a module (what it exports to the world).
6
- * Outputs: .mikk/diagrams/capsules/{moduleId}.mmd
7
- */
8
- export class CapsuleDiagramGenerator {
9
- constructor(
10
- private contract: MikkContract,
11
- private lock: MikkLock
12
- ) { }
13
-
14
- generate(moduleId: string): string {
15
- const module = this.contract.declared.modules.find(m => m.id === moduleId)
16
- if (!module) return `%% Module "${moduleId}" not found`
17
-
18
- const lines: string[] = []
19
- lines.push('graph LR')
20
- lines.push('')
21
-
22
- // Module capsule (subgraph)
23
- lines.push(` subgraph ${this.sanitizeId(moduleId)}["📦 ${module.name}"]`)
24
- lines.push(` direction TB`)
25
-
26
- // Find exported functions (those called by functions in other modules)
27
- const moduleFunctions = Object.values(this.lock.functions).filter(f => f.moduleId === moduleId)
28
- const exportedFns = moduleFunctions.filter(fn =>
29
- fn.calledBy.some(callerId => {
30
- const caller = this.lock.functions[callerId]
31
- return caller && caller.moduleId !== moduleId
32
- })
33
- )
34
-
35
- // Internal functions
36
- const internalFns = moduleFunctions.filter(fn => !exportedFns.includes(fn))
37
-
38
- if (exportedFns.length > 0) {
39
- lines.push(` subgraph public["🔓 Public API"]`)
40
- for (const fn of exportedFns) {
41
- lines.push(` ${this.sanitizeId(fn.id)}["${fn.name}"]`)
42
- }
43
- lines.push(' end')
44
- }
45
-
46
- if (internalFns.length > 0 && internalFns.length <= 10) {
47
- lines.push(` subgraph internal["🔒 Internal"]`)
48
- for (const fn of internalFns) {
49
- lines.push(` ${this.sanitizeId(fn.id)}["${fn.name}"]`)
50
- }
51
- lines.push(' end')
52
- } else if (internalFns.length > 10) {
53
- lines.push(` internal["🔒 ${internalFns.length} internal functions"]`)
54
- }
55
-
56
- lines.push(' end')
57
- lines.push('')
58
-
59
- // Show external consumers
60
- for (const fn of exportedFns) {
61
- for (const callerId of fn.calledBy) {
62
- const caller = this.lock.functions[callerId]
63
- if (caller && caller.moduleId !== moduleId) {
64
- lines.push(` ext_${this.sanitizeId(callerId)}["${caller.name}<br/>(${caller.moduleId})"] --> ${this.sanitizeId(fn.id)}`)
65
- }
66
- }
67
- }
68
-
69
- lines.push('')
70
- lines.push(' classDef publicApi fill:#27ae60,stroke:#2c3e50,color:#fff')
71
- lines.push(' classDef internalApi fill:#7f8c8d,stroke:#2c3e50,color:#fff')
72
-
73
- return lines.join('\n')
74
- }
75
-
76
- private sanitizeId(id: string): string {
77
- return id.replace(/[^a-zA-Z0-9_]/g, '_')
78
- }
79
- }
1
+ import type { MikkContract, MikkLock } from '@getmikk/core'
2
+
3
+ /**
4
+ * CapsuleDiagramGenerator — generates capsule diagrams that show
5
+ * the public API surface of a module (what it exports to the world).
6
+ * Outputs: .mikk/diagrams/capsules/{moduleId}.mmd
7
+ */
8
+ export class CapsuleDiagramGenerator {
9
+ constructor(
10
+ private contract: MikkContract,
11
+ private lock: MikkLock
12
+ ) { }
13
+
14
+ generate(moduleId: string): string {
15
+ const module = this.contract.declared.modules.find(m => m.id === moduleId)
16
+ if (!module) return `%% Module "${moduleId}" not found`
17
+
18
+ const lines: string[] = []
19
+ lines.push('graph LR')
20
+ lines.push('')
21
+
22
+ // Module capsule (subgraph)
23
+ lines.push(` subgraph ${this.sanitizeId(moduleId)}["📦 ${module.name}"]`)
24
+ lines.push(` direction TB`)
25
+
26
+ // Find exported functions (those called by functions in other modules)
27
+ const moduleFunctions = Object.values(this.lock.functions).filter(f => f.moduleId === moduleId)
28
+ const exportedFns = moduleFunctions.filter(fn =>
29
+ fn.calledBy.some(callerId => {
30
+ const caller = this.lock.functions[callerId]
31
+ return caller && caller.moduleId !== moduleId
32
+ })
33
+ )
34
+
35
+ // Internal functions
36
+ const internalFns = moduleFunctions.filter(fn => !exportedFns.includes(fn))
37
+
38
+ if (exportedFns.length > 0) {
39
+ lines.push(` subgraph public["🔓 Public API"]`)
40
+ for (const fn of exportedFns) {
41
+ lines.push(` ${this.sanitizeId(fn.id)}["${fn.name}"]`)
42
+ }
43
+ lines.push(' end')
44
+ }
45
+
46
+ if (internalFns.length > 0 && internalFns.length <= 10) {
47
+ lines.push(` subgraph internal["🔒 Internal"]`)
48
+ for (const fn of internalFns) {
49
+ lines.push(` ${this.sanitizeId(fn.id)}["${fn.name}"]`)
50
+ }
51
+ lines.push(' end')
52
+ } else if (internalFns.length > 10) {
53
+ lines.push(` internal["🔒 ${internalFns.length} internal functions"]`)
54
+ }
55
+
56
+ lines.push(' end')
57
+ lines.push('')
58
+
59
+ // Show external consumers
60
+ for (const fn of exportedFns) {
61
+ for (const callerId of fn.calledBy) {
62
+ const caller = this.lock.functions[callerId]
63
+ if (caller && caller.moduleId !== moduleId) {
64
+ lines.push(` ext_${this.sanitizeId(callerId)}["${caller.name}<br/>(${caller.moduleId})"] --> ${this.sanitizeId(fn.id)}`)
65
+ }
66
+ }
67
+ }
68
+
69
+ lines.push('')
70
+ lines.push(' classDef publicApi fill:#27ae60,stroke:#2c3e50,color:#fff')
71
+ lines.push(' classDef internalApi fill:#7f8c8d,stroke:#2c3e50,color:#fff')
72
+
73
+ return lines.join('\n')
74
+ }
75
+
76
+ private sanitizeId(id: string): string {
77
+ return id.replace(/[^a-zA-Z0-9_]/g, '_')
78
+ }
79
+ }
@@ -1,97 +1,97 @@
1
- import type { MikkContract, MikkLock } from '@getmikk/core'
2
-
3
- /**
4
- * DependencyMatrixGenerator — generates an N×N dependency matrix
5
- * showing how many cross-module calls exist between each pair of modules.
6
- * Useful for spotting hidden coupling between modules.
7
- *
8
- * Output: a Mermaid block diagram with a matrix-like layout or
9
- * a structured text table that can be rendered in documentation.
10
- */
11
- export class DependencyMatrixGenerator {
12
- constructor(
13
- private contract: MikkContract,
14
- private lock: MikkLock
15
- ) { }
16
-
17
- /**
18
- * Generate a Mermaid diagram showing the dependency matrix.
19
- * Uses a graph with weighted edges between all module pairs.
20
- */
21
- generate(): string {
22
- const modules = this.contract.declared.modules
23
- const matrix = this.computeMatrix()
24
- const lines: string[] = []
25
-
26
- lines.push('graph LR')
27
- lines.push('')
28
-
29
- // Module nodes
30
- for (const mod of modules) {
31
- const fnCount = Object.values(this.lock.functions)
32
- .filter(f => f.moduleId === mod.id).length
33
- lines.push(` ${this.sanitizeId(mod.id)}["${mod.name}<br/>${fnCount} fn"]`)
34
- }
35
- lines.push('')
36
-
37
- // Weighted edges
38
- for (const [key, count] of matrix) {
39
- const [fromId, toId] = key.split('|')
40
- if (count > 0) {
41
- const thickness = count > 10 ? '==>' : count > 3 ? '-->' : '-.->'
42
- lines.push(` ${this.sanitizeId(fromId)} ${thickness}|${count}| ${this.sanitizeId(toId)}`)
43
- }
44
- }
45
-
46
- lines.push('')
47
- lines.push(' classDef default fill:#ecf0f1,stroke:#34495e,color:#2c3e50')
48
-
49
- return lines.join('\n')
50
- }
51
-
52
- /**
53
- * Generate a markdown table showing the N×N dependency matrix.
54
- * Useful for claude.md or documentation.
55
- */
56
- generateTable(): string {
57
- const modules = this.contract.declared.modules
58
- const matrix = this.computeMatrix()
59
- const lines: string[] = []
60
-
61
- // Header
62
- lines.push('| From \\ To | ' + modules.map(m => m.name).join(' | ') + ' |')
63
- lines.push('| --- | ' + modules.map(() => '---').join(' | ') + ' |')
64
-
65
- // Rows
66
- for (const fromMod of modules) {
67
- const cells = modules.map(toMod => {
68
- if (fromMod.id === toMod.id) return '-'
69
- const count = matrix.get(`${fromMod.id}|${toMod.id}`) || 0
70
- return count > 0 ? String(count) : '0'
71
- })
72
- lines.push(`| **${fromMod.name}** | ${cells.join(' | ')} |`)
73
- }
74
-
75
- return lines.join('\n')
76
- }
77
-
78
- private computeMatrix(): Map<string, number> {
79
- const counts = new Map<string, number>()
80
-
81
- for (const fn of Object.values(this.lock.functions)) {
82
- for (const callTarget of fn.calls) {
83
- const targetFn = this.lock.functions[callTarget]
84
- if (targetFn && fn.moduleId !== targetFn.moduleId) {
85
- const key = `${fn.moduleId}|${targetFn.moduleId}`
86
- counts.set(key, (counts.get(key) || 0) + 1)
87
- }
88
- }
89
- }
90
-
91
- return counts
92
- }
93
-
94
- private sanitizeId(id: string): string {
95
- return id.replace(/[^a-zA-Z0-9_]/g, '_')
96
- }
97
- }
1
+ import type { MikkContract, MikkLock } from '@getmikk/core'
2
+
3
+ /**
4
+ * DependencyMatrixGenerator — generates an N×N dependency matrix
5
+ * showing how many cross-module calls exist between each pair of modules.
6
+ * Useful for spotting hidden coupling between modules.
7
+ *
8
+ * Output: a Mermaid block diagram with a matrix-like layout or
9
+ * a structured text table that can be rendered in documentation.
10
+ */
11
+ export class DependencyMatrixGenerator {
12
+ constructor(
13
+ private contract: MikkContract,
14
+ private lock: MikkLock
15
+ ) { }
16
+
17
+ /**
18
+ * Generate a Mermaid diagram showing the dependency matrix.
19
+ * Uses a graph with weighted edges between all module pairs.
20
+ */
21
+ generate(): string {
22
+ const modules = this.contract.declared.modules
23
+ const matrix = this.computeMatrix()
24
+ const lines: string[] = []
25
+
26
+ lines.push('graph LR')
27
+ lines.push('')
28
+
29
+ // Module nodes
30
+ for (const mod of modules) {
31
+ const fnCount = Object.values(this.lock.functions)
32
+ .filter(f => f.moduleId === mod.id).length
33
+ lines.push(` ${this.sanitizeId(mod.id)}["${mod.name}<br/>${fnCount} fn"]`)
34
+ }
35
+ lines.push('')
36
+
37
+ // Weighted edges
38
+ for (const [key, count] of matrix) {
39
+ const [fromId, toId] = key.split('|')
40
+ if (count > 0) {
41
+ const thickness = count > 10 ? '==>' : count > 3 ? '-->' : '-.->'
42
+ lines.push(` ${this.sanitizeId(fromId)} ${thickness}|${count}| ${this.sanitizeId(toId)}`)
43
+ }
44
+ }
45
+
46
+ lines.push('')
47
+ lines.push(' classDef default fill:#ecf0f1,stroke:#34495e,color:#2c3e50')
48
+
49
+ return lines.join('\n')
50
+ }
51
+
52
+ /**
53
+ * Generate a markdown table showing the N×N dependency matrix.
54
+ * Useful for claude.md or documentation.
55
+ */
56
+ generateTable(): string {
57
+ const modules = this.contract.declared.modules
58
+ const matrix = this.computeMatrix()
59
+ const lines: string[] = []
60
+
61
+ // Header
62
+ lines.push('| From \\ To | ' + modules.map(m => m.name).join(' | ') + ' |')
63
+ lines.push('| --- | ' + modules.map(() => '---').join(' | ') + ' |')
64
+
65
+ // Rows
66
+ for (const fromMod of modules) {
67
+ const cells = modules.map(toMod => {
68
+ if (fromMod.id === toMod.id) return '-'
69
+ const count = matrix.get(`${fromMod.id}|${toMod.id}`) || 0
70
+ return count > 0 ? String(count) : '0'
71
+ })
72
+ lines.push(`| **${fromMod.name}** | ${cells.join(' | ')} |`)
73
+ }
74
+
75
+ return lines.join('\n')
76
+ }
77
+
78
+ private computeMatrix(): Map<string, number> {
79
+ const counts = new Map<string, number>()
80
+
81
+ for (const fn of Object.values(this.lock.functions)) {
82
+ for (const callTarget of fn.calls) {
83
+ const targetFn = this.lock.functions[callTarget]
84
+ if (targetFn && fn.moduleId !== targetFn.moduleId) {
85
+ const key = `${fn.moduleId}|${targetFn.moduleId}`
86
+ counts.set(key, (counts.get(key) || 0) + 1)
87
+ }
88
+ }
89
+ }
90
+
91
+ return counts
92
+ }
93
+
94
+ private sanitizeId(id: string): string {
95
+ return id.replace(/[^a-zA-Z0-9_]/g, '_')
96
+ }
97
+ }