@getmikk/core 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 +431 -0
- package/package.json +6 -2
- package/src/contract/contract-generator.ts +85 -85
- package/src/contract/contract-reader.ts +28 -28
- package/src/contract/contract-writer.ts +114 -114
- package/src/contract/index.ts +12 -12
- package/src/contract/lock-compiler.ts +221 -221
- package/src/contract/lock-reader.ts +34 -34
- package/src/contract/schema.ts +147 -147
- package/src/graph/cluster-detector.ts +312 -312
- package/src/graph/graph-builder.ts +211 -211
- package/src/graph/impact-analyzer.ts +55 -55
- package/src/graph/index.ts +4 -4
- package/src/graph/types.ts +59 -59
- package/src/hash/file-hasher.ts +30 -30
- package/src/hash/hash-store.ts +119 -119
- package/src/hash/index.ts +3 -3
- package/src/hash/tree-hasher.ts +20 -20
- package/src/index.ts +12 -12
- package/src/parser/base-parser.ts +16 -16
- package/src/parser/boundary-checker.ts +211 -211
- package/src/parser/index.ts +46 -46
- package/src/parser/types.ts +90 -90
- package/src/parser/typescript/ts-extractor.ts +543 -543
- package/src/parser/typescript/ts-parser.ts +41 -41
- package/src/parser/typescript/ts-resolver.ts +86 -86
- package/src/utils/errors.ts +42 -42
- package/src/utils/fs.ts +75 -75
- package/src/utils/fuzzy-match.ts +186 -186
- package/src/utils/logger.ts +36 -36
- package/src/utils/minimatch.ts +19 -19
- package/tests/contract.test.ts +134 -134
- package/tests/fixtures/simple-api/package.json +5 -5
- package/tests/fixtures/simple-api/src/auth/middleware.ts +9 -9
- package/tests/fixtures/simple-api/src/auth/verify.ts +6 -6
- package/tests/fixtures/simple-api/src/index.ts +9 -9
- package/tests/fixtures/simple-api/src/utils/jwt.ts +3 -3
- package/tests/fixtures/simple-api/tsconfig.json +8 -8
- package/tests/fuzzy-match.test.ts +142 -142
- package/tests/graph.test.ts +169 -169
- package/tests/hash.test.ts +49 -49
- package/tests/helpers.ts +83 -83
- package/tests/parser.test.ts +218 -218
- package/tsconfig.json +15 -15
|
@@ -1,212 +1,212 @@
|
|
|
1
|
-
import * as path from 'node:path'
|
|
2
|
-
import type { MikkContract, MikkLock, MikkLockFunction } from '@getmikk/core'
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Types
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
export type ViolationSeverity = 'error' | 'warning'
|
|
9
|
-
|
|
10
|
-
export interface BoundaryViolation {
|
|
11
|
-
/** The function making the illegal import */
|
|
12
|
-
from: {
|
|
13
|
-
functionId: string
|
|
14
|
-
functionName: string
|
|
15
|
-
file: string
|
|
16
|
-
moduleId: string
|
|
17
|
-
moduleName: string
|
|
18
|
-
}
|
|
19
|
-
/** The function being illegally called */
|
|
20
|
-
to: {
|
|
21
|
-
functionId: string
|
|
22
|
-
functionName: string
|
|
23
|
-
file: string
|
|
24
|
-
moduleId: string
|
|
25
|
-
moduleName: string
|
|
26
|
-
}
|
|
27
|
-
rule: string
|
|
28
|
-
severity: ViolationSeverity
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface BoundaryCheckResult {
|
|
32
|
-
pass: boolean
|
|
33
|
-
violations: BoundaryViolation[]
|
|
34
|
-
summary: string
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
// Rule parsing
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Parse constraint strings into structured allow/deny rules.
|
|
43
|
-
*
|
|
44
|
-
* Supported syntax in mikk.json constraints:
|
|
45
|
-
* "module:auth cannot import module:payments"
|
|
46
|
-
* "module:cli cannot import module:db"
|
|
47
|
-
* "module:core has no imports" → core is completely isolated
|
|
48
|
-
* "module:api can only import module:core, module:utils"
|
|
49
|
-
*/
|
|
50
|
-
interface ParsedRule {
|
|
51
|
-
type: 'deny' | 'allow_only' | 'isolated'
|
|
52
|
-
fromModuleId: string
|
|
53
|
-
toModuleIds: string[] // For deny: what's forbidden. For allow_only: what's allowed.
|
|
54
|
-
raw: string
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function parseConstraint(constraint: string): ParsedRule | null {
|
|
58
|
-
const c = constraint.trim().toLowerCase()
|
|
59
|
-
|
|
60
|
-
// "module:X cannot import module:Y" or "module:X cannot import module:Y, module:Z"
|
|
61
|
-
const denyMatch = c.match(/^module:(\S+)\s+cannot\s+import\s+(.+)$/)
|
|
62
|
-
if (denyMatch) {
|
|
63
|
-
const toModules = denyMatch[2]
|
|
64
|
-
.split(',')
|
|
65
|
-
.map(s => s.trim().replace('module:', ''))
|
|
66
|
-
.filter(Boolean)
|
|
67
|
-
return { type: 'deny', fromModuleId: denyMatch[1], toModuleIds: toModules, raw: constraint }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// "module:X can only import module:A, module:B"
|
|
71
|
-
const allowOnlyMatch = c.match(/^module:(\S+)\s+can\s+only\s+import\s+(.+)$/)
|
|
72
|
-
if (allowOnlyMatch) {
|
|
73
|
-
const toModules = allowOnlyMatch[2]
|
|
74
|
-
.split(',')
|
|
75
|
-
.map(s => s.trim().replace('module:', ''))
|
|
76
|
-
.filter(Boolean)
|
|
77
|
-
return { type: 'allow_only', fromModuleId: allowOnlyMatch[1], toModuleIds: toModules, raw: constraint }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// "module:X has no imports" or "module:X is isolated"
|
|
81
|
-
const isolatedMatch = c.match(/^module:(\S+)\s+(has\s+no\s+imports|is\s+isolated)$/)
|
|
82
|
-
if (isolatedMatch) {
|
|
83
|
-
return { type: 'isolated', fromModuleId: isolatedMatch[1], toModuleIds: [], raw: constraint }
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return null // unrecognized constraint — skip silently
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
// BoundaryChecker
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* BoundaryChecker — walks the lock file's call graph and checks every
|
|
95
|
-
* cross-module call against the rules declared in mikk.json constraints.
|
|
96
|
-
*
|
|
97
|
-
* This is the CI-ready enforcement layer.
|
|
98
|
-
*/
|
|
99
|
-
export class BoundaryChecker {
|
|
100
|
-
private rules: ParsedRule[]
|
|
101
|
-
private moduleNames: Map<string, string> // id → name
|
|
102
|
-
|
|
103
|
-
constructor(
|
|
104
|
-
private contract: MikkContract,
|
|
105
|
-
private lock: MikkLock
|
|
106
|
-
) {
|
|
107
|
-
this.rules = contract.declared.constraints
|
|
108
|
-
.map(parseConstraint)
|
|
109
|
-
.filter((r): r is ParsedRule => r !== null)
|
|
110
|
-
|
|
111
|
-
this.moduleNames = new Map(contract.declared.modules.map(m => [m.id, m.name]))
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Run boundary check. Returns pass/fail + all violations. */
|
|
115
|
-
check(): BoundaryCheckResult {
|
|
116
|
-
const violations: BoundaryViolation[] = []
|
|
117
|
-
|
|
118
|
-
// Collect all cross-module calls
|
|
119
|
-
for (const fn of Object.values(this.lock.functions)) {
|
|
120
|
-
for (const calleeId of fn.calls) {
|
|
121
|
-
const callee = this.lock.functions[calleeId]
|
|
122
|
-
if (!callee) continue
|
|
123
|
-
if (fn.moduleId === callee.moduleId) continue // same module — fine
|
|
124
|
-
|
|
125
|
-
// Check this cross-module call against all parsed rules
|
|
126
|
-
const violation = this.checkCall(fn, callee)
|
|
127
|
-
if (violation) violations.push(violation)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const errorCount = violations.filter(v => v.severity === 'error').length
|
|
132
|
-
const warnCount = violations.filter(v => v.severity === 'warning').length
|
|
133
|
-
|
|
134
|
-
const summary = violations.length === 0
|
|
135
|
-
? `✓ All module boundaries respected (${Object.keys(this.lock.functions).length} functions checked)`
|
|
136
|
-
: `✗ ${errorCount} boundary error(s), ${warnCount} warning(s) found`
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
pass: errorCount === 0,
|
|
140
|
-
violations,
|
|
141
|
-
summary,
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Check a single cross-module call against parsed rules.
|
|
147
|
-
* Returns a violation if the call is forbidden, null if it's allowed.
|
|
148
|
-
*/
|
|
149
|
-
private checkCall(
|
|
150
|
-
caller: MikkLockFunction,
|
|
151
|
-
callee: MikkLockFunction
|
|
152
|
-
): BoundaryViolation | null {
|
|
153
|
-
for (const rule of this.rules) {
|
|
154
|
-
if (rule.fromModuleId !== caller.moduleId) continue
|
|
155
|
-
|
|
156
|
-
let forbidden = false
|
|
157
|
-
let ruleDesc = rule.raw
|
|
158
|
-
|
|
159
|
-
if (rule.type === 'isolated') {
|
|
160
|
-
// Module may not call anything outside itself
|
|
161
|
-
forbidden = true
|
|
162
|
-
} else if (rule.type === 'deny') {
|
|
163
|
-
// Module may not call into these specific modules
|
|
164
|
-
forbidden = rule.toModuleIds.includes(callee.moduleId)
|
|
165
|
-
} else if (rule.type === 'allow_only') {
|
|
166
|
-
// Module may ONLY call into the listed modules (+ itself)
|
|
167
|
-
forbidden = !rule.toModuleIds.includes(callee.moduleId)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (forbidden) {
|
|
171
|
-
return {
|
|
172
|
-
from: {
|
|
173
|
-
functionId: caller.id,
|
|
174
|
-
functionName: caller.name,
|
|
175
|
-
file: caller.file,
|
|
176
|
-
moduleId: caller.moduleId,
|
|
177
|
-
moduleName: this.moduleNames.get(caller.moduleId) ?? caller.moduleId,
|
|
178
|
-
},
|
|
179
|
-
to: {
|
|
180
|
-
functionId: callee.id,
|
|
181
|
-
functionName: callee.name,
|
|
182
|
-
file: callee.file,
|
|
183
|
-
moduleId: callee.moduleId,
|
|
184
|
-
moduleName: this.moduleNames.get(callee.moduleId) ?? callee.moduleId,
|
|
185
|
-
},
|
|
186
|
-
rule: ruleDesc,
|
|
187
|
-
severity: 'error',
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return null
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** Return all cross-module call pairs (useful for generating allow rules) */
|
|
195
|
-
allCrossModuleCalls(): { from: string; to: string; count: number }[] {
|
|
196
|
-
const counts = new Map<string, number>()
|
|
197
|
-
for (const fn of Object.values(this.lock.functions)) {
|
|
198
|
-
for (const calleeId of fn.calls) {
|
|
199
|
-
const callee = this.lock.functions[calleeId]
|
|
200
|
-
if (!callee || fn.moduleId === callee.moduleId) continue
|
|
201
|
-
const key = `${fn.moduleId}→${callee.moduleId}`
|
|
202
|
-
counts.set(key, (counts.get(key) ?? 0) + 1)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return [...counts.entries()]
|
|
206
|
-
.map(([key, count]) => {
|
|
207
|
-
const [from, to] = key.split('→')
|
|
208
|
-
return { from, to, count }
|
|
209
|
-
})
|
|
210
|
-
.sort((a, b) => b.count - a.count)
|
|
211
|
-
}
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import type { MikkContract, MikkLock, MikkLockFunction } from '@getmikk/core'
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Types
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
export type ViolationSeverity = 'error' | 'warning'
|
|
9
|
+
|
|
10
|
+
export interface BoundaryViolation {
|
|
11
|
+
/** The function making the illegal import */
|
|
12
|
+
from: {
|
|
13
|
+
functionId: string
|
|
14
|
+
functionName: string
|
|
15
|
+
file: string
|
|
16
|
+
moduleId: string
|
|
17
|
+
moduleName: string
|
|
18
|
+
}
|
|
19
|
+
/** The function being illegally called */
|
|
20
|
+
to: {
|
|
21
|
+
functionId: string
|
|
22
|
+
functionName: string
|
|
23
|
+
file: string
|
|
24
|
+
moduleId: string
|
|
25
|
+
moduleName: string
|
|
26
|
+
}
|
|
27
|
+
rule: string
|
|
28
|
+
severity: ViolationSeverity
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BoundaryCheckResult {
|
|
32
|
+
pass: boolean
|
|
33
|
+
violations: BoundaryViolation[]
|
|
34
|
+
summary: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Rule parsing
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse constraint strings into structured allow/deny rules.
|
|
43
|
+
*
|
|
44
|
+
* Supported syntax in mikk.json constraints:
|
|
45
|
+
* "module:auth cannot import module:payments"
|
|
46
|
+
* "module:cli cannot import module:db"
|
|
47
|
+
* "module:core has no imports" → core is completely isolated
|
|
48
|
+
* "module:api can only import module:core, module:utils"
|
|
49
|
+
*/
|
|
50
|
+
interface ParsedRule {
|
|
51
|
+
type: 'deny' | 'allow_only' | 'isolated'
|
|
52
|
+
fromModuleId: string
|
|
53
|
+
toModuleIds: string[] // For deny: what's forbidden. For allow_only: what's allowed.
|
|
54
|
+
raw: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseConstraint(constraint: string): ParsedRule | null {
|
|
58
|
+
const c = constraint.trim().toLowerCase()
|
|
59
|
+
|
|
60
|
+
// "module:X cannot import module:Y" or "module:X cannot import module:Y, module:Z"
|
|
61
|
+
const denyMatch = c.match(/^module:(\S+)\s+cannot\s+import\s+(.+)$/)
|
|
62
|
+
if (denyMatch) {
|
|
63
|
+
const toModules = denyMatch[2]
|
|
64
|
+
.split(',')
|
|
65
|
+
.map(s => s.trim().replace('module:', ''))
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
return { type: 'deny', fromModuleId: denyMatch[1], toModuleIds: toModules, raw: constraint }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// "module:X can only import module:A, module:B"
|
|
71
|
+
const allowOnlyMatch = c.match(/^module:(\S+)\s+can\s+only\s+import\s+(.+)$/)
|
|
72
|
+
if (allowOnlyMatch) {
|
|
73
|
+
const toModules = allowOnlyMatch[2]
|
|
74
|
+
.split(',')
|
|
75
|
+
.map(s => s.trim().replace('module:', ''))
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
return { type: 'allow_only', fromModuleId: allowOnlyMatch[1], toModuleIds: toModules, raw: constraint }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// "module:X has no imports" or "module:X is isolated"
|
|
81
|
+
const isolatedMatch = c.match(/^module:(\S+)\s+(has\s+no\s+imports|is\s+isolated)$/)
|
|
82
|
+
if (isolatedMatch) {
|
|
83
|
+
return { type: 'isolated', fromModuleId: isolatedMatch[1], toModuleIds: [], raw: constraint }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null // unrecognized constraint — skip silently
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// BoundaryChecker
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* BoundaryChecker — walks the lock file's call graph and checks every
|
|
95
|
+
* cross-module call against the rules declared in mikk.json constraints.
|
|
96
|
+
*
|
|
97
|
+
* This is the CI-ready enforcement layer.
|
|
98
|
+
*/
|
|
99
|
+
export class BoundaryChecker {
|
|
100
|
+
private rules: ParsedRule[]
|
|
101
|
+
private moduleNames: Map<string, string> // id → name
|
|
102
|
+
|
|
103
|
+
constructor(
|
|
104
|
+
private contract: MikkContract,
|
|
105
|
+
private lock: MikkLock
|
|
106
|
+
) {
|
|
107
|
+
this.rules = contract.declared.constraints
|
|
108
|
+
.map(parseConstraint)
|
|
109
|
+
.filter((r): r is ParsedRule => r !== null)
|
|
110
|
+
|
|
111
|
+
this.moduleNames = new Map(contract.declared.modules.map(m => [m.id, m.name]))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Run boundary check. Returns pass/fail + all violations. */
|
|
115
|
+
check(): BoundaryCheckResult {
|
|
116
|
+
const violations: BoundaryViolation[] = []
|
|
117
|
+
|
|
118
|
+
// Collect all cross-module calls
|
|
119
|
+
for (const fn of Object.values(this.lock.functions)) {
|
|
120
|
+
for (const calleeId of fn.calls) {
|
|
121
|
+
const callee = this.lock.functions[calleeId]
|
|
122
|
+
if (!callee) continue
|
|
123
|
+
if (fn.moduleId === callee.moduleId) continue // same module — fine
|
|
124
|
+
|
|
125
|
+
// Check this cross-module call against all parsed rules
|
|
126
|
+
const violation = this.checkCall(fn, callee)
|
|
127
|
+
if (violation) violations.push(violation)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const errorCount = violations.filter(v => v.severity === 'error').length
|
|
132
|
+
const warnCount = violations.filter(v => v.severity === 'warning').length
|
|
133
|
+
|
|
134
|
+
const summary = violations.length === 0
|
|
135
|
+
? `✓ All module boundaries respected (${Object.keys(this.lock.functions).length} functions checked)`
|
|
136
|
+
: `✗ ${errorCount} boundary error(s), ${warnCount} warning(s) found`
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
pass: errorCount === 0,
|
|
140
|
+
violations,
|
|
141
|
+
summary,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check a single cross-module call against parsed rules.
|
|
147
|
+
* Returns a violation if the call is forbidden, null if it's allowed.
|
|
148
|
+
*/
|
|
149
|
+
private checkCall(
|
|
150
|
+
caller: MikkLockFunction,
|
|
151
|
+
callee: MikkLockFunction
|
|
152
|
+
): BoundaryViolation | null {
|
|
153
|
+
for (const rule of this.rules) {
|
|
154
|
+
if (rule.fromModuleId !== caller.moduleId) continue
|
|
155
|
+
|
|
156
|
+
let forbidden = false
|
|
157
|
+
let ruleDesc = rule.raw
|
|
158
|
+
|
|
159
|
+
if (rule.type === 'isolated') {
|
|
160
|
+
// Module may not call anything outside itself
|
|
161
|
+
forbidden = true
|
|
162
|
+
} else if (rule.type === 'deny') {
|
|
163
|
+
// Module may not call into these specific modules
|
|
164
|
+
forbidden = rule.toModuleIds.includes(callee.moduleId)
|
|
165
|
+
} else if (rule.type === 'allow_only') {
|
|
166
|
+
// Module may ONLY call into the listed modules (+ itself)
|
|
167
|
+
forbidden = !rule.toModuleIds.includes(callee.moduleId)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (forbidden) {
|
|
171
|
+
return {
|
|
172
|
+
from: {
|
|
173
|
+
functionId: caller.id,
|
|
174
|
+
functionName: caller.name,
|
|
175
|
+
file: caller.file,
|
|
176
|
+
moduleId: caller.moduleId,
|
|
177
|
+
moduleName: this.moduleNames.get(caller.moduleId) ?? caller.moduleId,
|
|
178
|
+
},
|
|
179
|
+
to: {
|
|
180
|
+
functionId: callee.id,
|
|
181
|
+
functionName: callee.name,
|
|
182
|
+
file: callee.file,
|
|
183
|
+
moduleId: callee.moduleId,
|
|
184
|
+
moduleName: this.moduleNames.get(callee.moduleId) ?? callee.moduleId,
|
|
185
|
+
},
|
|
186
|
+
rule: ruleDesc,
|
|
187
|
+
severity: 'error',
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Return all cross-module call pairs (useful for generating allow rules) */
|
|
195
|
+
allCrossModuleCalls(): { from: string; to: string; count: number }[] {
|
|
196
|
+
const counts = new Map<string, number>()
|
|
197
|
+
for (const fn of Object.values(this.lock.functions)) {
|
|
198
|
+
for (const calleeId of fn.calls) {
|
|
199
|
+
const callee = this.lock.functions[calleeId]
|
|
200
|
+
if (!callee || fn.moduleId === callee.moduleId) continue
|
|
201
|
+
const key = `${fn.moduleId}→${callee.moduleId}`
|
|
202
|
+
counts.set(key, (counts.get(key) ?? 0) + 1)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return [...counts.entries()]
|
|
206
|
+
.map(([key, count]) => {
|
|
207
|
+
const [from, to] = key.split('→')
|
|
208
|
+
return { from, to, count }
|
|
209
|
+
})
|
|
210
|
+
.sort((a, b) => b.count - a.count)
|
|
211
|
+
}
|
|
212
212
|
}
|
package/src/parser/index.ts
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import * as path from 'node:path'
|
|
2
|
-
import { BaseParser } from './base-parser.js'
|
|
3
|
-
import { TypeScriptParser } from './typescript/ts-parser.js'
|
|
4
|
-
import { UnsupportedLanguageError } from '../utils/errors.js'
|
|
5
|
-
import type { ParsedFile } from './types.js'
|
|
6
|
-
|
|
7
|
-
export type { ParsedFile, ParsedFunction, ParsedImport, ParsedExport, ParsedClass, ParsedParam } from './types.js'
|
|
8
|
-
export { BaseParser } from './base-parser.js'
|
|
9
|
-
export { TypeScriptParser } from './typescript/ts-parser.js'
|
|
10
|
-
export { TypeScriptExtractor } from './typescript/ts-extractor.js'
|
|
11
|
-
export { TypeScriptResolver } from './typescript/ts-resolver.js'
|
|
12
|
-
export { BoundaryChecker } from './boundary-checker.js'
|
|
13
|
-
|
|
14
|
-
/** Get the appropriate parser for a file based on its extension */
|
|
15
|
-
export function getParser(filePath: string): BaseParser {
|
|
16
|
-
const ext = path.extname(filePath)
|
|
17
|
-
switch (ext) {
|
|
18
|
-
case '.ts':
|
|
19
|
-
case '.tsx':
|
|
20
|
-
return new TypeScriptParser()
|
|
21
|
-
default:
|
|
22
|
-
throw new UnsupportedLanguageError(ext)
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Parse multiple files and resolve imports across them */
|
|
27
|
-
export async function parseFiles(
|
|
28
|
-
filePaths: string[],
|
|
29
|
-
projectRoot: string,
|
|
30
|
-
readFile: (fp: string) => Promise<string>
|
|
31
|
-
): Promise<ParsedFile[]> {
|
|
32
|
-
const tsParser = new TypeScriptParser()
|
|
33
|
-
const files: ParsedFile[] = []
|
|
34
|
-
|
|
35
|
-
for (const fp of filePaths) {
|
|
36
|
-
const ext = path.extname(fp)
|
|
37
|
-
if (ext === '.ts' || ext === '.tsx') {
|
|
38
|
-
const content = await readFile(path.join(projectRoot, fp))
|
|
39
|
-
const parsed = tsParser.parse(fp, content)
|
|
40
|
-
files.push(parsed)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Resolve all imports after all files are parsed
|
|
45
|
-
return tsParser.resolveImports(files, projectRoot)
|
|
46
|
-
}
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import { BaseParser } from './base-parser.js'
|
|
3
|
+
import { TypeScriptParser } from './typescript/ts-parser.js'
|
|
4
|
+
import { UnsupportedLanguageError } from '../utils/errors.js'
|
|
5
|
+
import type { ParsedFile } from './types.js'
|
|
6
|
+
|
|
7
|
+
export type { ParsedFile, ParsedFunction, ParsedImport, ParsedExport, ParsedClass, ParsedParam } from './types.js'
|
|
8
|
+
export { BaseParser } from './base-parser.js'
|
|
9
|
+
export { TypeScriptParser } from './typescript/ts-parser.js'
|
|
10
|
+
export { TypeScriptExtractor } from './typescript/ts-extractor.js'
|
|
11
|
+
export { TypeScriptResolver } from './typescript/ts-resolver.js'
|
|
12
|
+
export { BoundaryChecker } from './boundary-checker.js'
|
|
13
|
+
|
|
14
|
+
/** Get the appropriate parser for a file based on its extension */
|
|
15
|
+
export function getParser(filePath: string): BaseParser {
|
|
16
|
+
const ext = path.extname(filePath)
|
|
17
|
+
switch (ext) {
|
|
18
|
+
case '.ts':
|
|
19
|
+
case '.tsx':
|
|
20
|
+
return new TypeScriptParser()
|
|
21
|
+
default:
|
|
22
|
+
throw new UnsupportedLanguageError(ext)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Parse multiple files and resolve imports across them */
|
|
27
|
+
export async function parseFiles(
|
|
28
|
+
filePaths: string[],
|
|
29
|
+
projectRoot: string,
|
|
30
|
+
readFile: (fp: string) => Promise<string>
|
|
31
|
+
): Promise<ParsedFile[]> {
|
|
32
|
+
const tsParser = new TypeScriptParser()
|
|
33
|
+
const files: ParsedFile[] = []
|
|
34
|
+
|
|
35
|
+
for (const fp of filePaths) {
|
|
36
|
+
const ext = path.extname(fp)
|
|
37
|
+
if (ext === '.ts' || ext === '.tsx') {
|
|
38
|
+
const content = await readFile(path.join(projectRoot, fp))
|
|
39
|
+
const parsed = tsParser.parse(fp, content)
|
|
40
|
+
files.push(parsed)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Resolve all imports after all files are parsed
|
|
45
|
+
return tsParser.resolveImports(files, projectRoot)
|
|
46
|
+
}
|