@getmikk/core 1.3.2 → 1.5.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/package.json +1 -1
- package/src/contract/contract-generator.ts +87 -8
- package/src/contract/lock-compiler.ts +174 -8
- package/src/contract/lock-reader.ts +269 -3
- package/src/contract/schema.ts +31 -0
- package/src/graph/cluster-detector.ts +286 -18
- package/src/graph/graph-builder.ts +2 -0
- package/src/graph/types.ts +2 -0
- package/src/index.ts +2 -1
- package/src/parser/boundary-checker.ts +74 -2
- package/src/parser/types.ts +11 -0
- package/src/parser/typescript/ts-extractor.ts +146 -8
- package/src/parser/typescript/ts-parser.ts +32 -5
- package/src/utils/fs.ts +586 -4
- package/tests/fs.test.ts +186 -0
- package/tests/helpers.ts +6 -0
package/src/utils/fs.ts
CHANGED
|
@@ -2,18 +2,376 @@ import * as fs from 'node:fs/promises'
|
|
|
2
2
|
import * as path from 'node:path'
|
|
3
3
|
import fg from 'fast-glob'
|
|
4
4
|
|
|
5
|
+
// ─── Well-known patterns for schema/config/route files ─────────────
|
|
6
|
+
// These are structural files an AI agent needs but aren't source code.
|
|
7
|
+
// Mikk auto-discovers them so the AI doesn't have to explore the filesystem.
|
|
8
|
+
// Patterns are language-agnostic — unused patterns simply return zero matches.
|
|
9
|
+
const CONTEXT_FILE_PATTERNS = [
|
|
10
|
+
// Data models / schemas — JS/TS
|
|
11
|
+
'**/prisma/schema.prisma',
|
|
12
|
+
'**/drizzle/**/*.ts',
|
|
13
|
+
'**/schema/**/*.{ts,js,graphql,gql,sql}',
|
|
14
|
+
'**/models/**/*.{ts,js}',
|
|
15
|
+
'**/*.schema.{ts,js}',
|
|
16
|
+
'**/*.model.{ts,js}',
|
|
17
|
+
// Data models / schemas — Python
|
|
18
|
+
'**/models.py',
|
|
19
|
+
'**/schemas.py',
|
|
20
|
+
'**/serializers.py',
|
|
21
|
+
'**/models/**/*.py',
|
|
22
|
+
// Data models / schemas — Ruby
|
|
23
|
+
'**/app/models/**/*.rb',
|
|
24
|
+
'**/db/schema.rb',
|
|
25
|
+
// Data models / schemas — Go / Rust / Java / PHP
|
|
26
|
+
'**/models/*.go',
|
|
27
|
+
'**/*_model.go',
|
|
28
|
+
'**/schema.rs',
|
|
29
|
+
'**/models.rs',
|
|
30
|
+
'**/entity/**/*.java',
|
|
31
|
+
'**/model/**/*.java',
|
|
32
|
+
'**/dto/**/*.java',
|
|
33
|
+
'**/Entities/**/*.php',
|
|
34
|
+
'**/Models/**/*.php',
|
|
35
|
+
// GraphQL / Proto
|
|
36
|
+
'**/*.graphql',
|
|
37
|
+
'**/*.gql',
|
|
38
|
+
'**/*.proto',
|
|
39
|
+
// API definitions
|
|
40
|
+
'**/openapi.{yaml,yml,json}',
|
|
41
|
+
'**/swagger.{yaml,yml,json}',
|
|
42
|
+
// Route definitions
|
|
43
|
+
'**/routes/**/*.{ts,js}',
|
|
44
|
+
'**/router.{ts,js}',
|
|
45
|
+
// Database migrations (latest only) — multi-language
|
|
46
|
+
'**/migrations/**/migration.sql',
|
|
47
|
+
'**/db/migrate/**/*.rb',
|
|
48
|
+
'**/alembic/**/*.py',
|
|
49
|
+
'**/migrations/**/*.sql',
|
|
50
|
+
// Type definitions
|
|
51
|
+
'**/types/**/*.{ts,js}',
|
|
52
|
+
'**/types.{ts,js}',
|
|
53
|
+
'**/interfaces/**/*.{ts,js}',
|
|
54
|
+
// Config files
|
|
55
|
+
'**/docker-compose.{yml,yaml}',
|
|
56
|
+
'**/Dockerfile',
|
|
57
|
+
'.env.example',
|
|
58
|
+
'.env.local.example',
|
|
59
|
+
// Schema definitions — general
|
|
60
|
+
'**/schema.{yaml,yml,json}',
|
|
61
|
+
'**/*.avsc',
|
|
62
|
+
'**/*.thrift',
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
const CONTEXT_FILE_IGNORE = [
|
|
66
|
+
// JavaScript / TypeScript
|
|
67
|
+
'**/node_modules/**',
|
|
68
|
+
'**/dist/**',
|
|
69
|
+
'**/.next/**',
|
|
70
|
+
'**/.nuxt/**',
|
|
71
|
+
'**/.svelte-kit/**',
|
|
72
|
+
'**/.astro/**',
|
|
73
|
+
'**/*.d.ts',
|
|
74
|
+
'**/*.test.{ts,js,tsx,jsx}',
|
|
75
|
+
'**/*.spec.{ts,js,tsx,jsx}',
|
|
76
|
+
// General
|
|
77
|
+
'**/build/**',
|
|
78
|
+
'**/coverage/**',
|
|
79
|
+
'**/.mikk/**',
|
|
80
|
+
'**/.git/**',
|
|
81
|
+
// Python
|
|
82
|
+
'**/__pycache__/**',
|
|
83
|
+
'**/*.pyc',
|
|
84
|
+
'**/venv/**',
|
|
85
|
+
'**/.venv/**',
|
|
86
|
+
'**/.tox/**',
|
|
87
|
+
// Go
|
|
88
|
+
'**/vendor/**',
|
|
89
|
+
// Rust / Java
|
|
90
|
+
'**/target/**',
|
|
91
|
+
// C# / .NET
|
|
92
|
+
'**/bin/**',
|
|
93
|
+
'**/obj/**',
|
|
94
|
+
// Ruby / PHP
|
|
95
|
+
'**/vendor/**',
|
|
96
|
+
// Elixir
|
|
97
|
+
'**/deps/**',
|
|
98
|
+
'**/_build/**',
|
|
99
|
+
// Gradle
|
|
100
|
+
'**/.gradle/**',
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
/** Category of a discovered context file */
|
|
104
|
+
export type ContextFileType = 'schema' | 'model' | 'types' | 'routes' | 'config' | 'api-spec' | 'migration' | 'docker'
|
|
105
|
+
|
|
106
|
+
/** A discovered context file with its content and inferred category */
|
|
107
|
+
export interface ContextFile {
|
|
108
|
+
/** Relative path from project root */
|
|
109
|
+
path: string
|
|
110
|
+
/** Raw content of the file */
|
|
111
|
+
content: string
|
|
112
|
+
/** Inferred category */
|
|
113
|
+
type: ContextFileType
|
|
114
|
+
/** File size in bytes */
|
|
115
|
+
size: number
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Maximum size (in bytes) for a single context file — skip huge files */
|
|
119
|
+
const MAX_CONTEXT_FILE_SIZE = 50_000 // ~50KB
|
|
120
|
+
|
|
121
|
+
// ─── .mikkignore support ───────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Read a .mikkignore file from the project root and parse it into
|
|
125
|
+
* fast-glob compatible ignore patterns.
|
|
126
|
+
*
|
|
127
|
+
* Syntax: gitignore-style.
|
|
128
|
+
* - Lines starting with # are comments
|
|
129
|
+
* - Blank lines are ignored
|
|
130
|
+
* - Patterns without / match anywhere in the path (e.g. "dist" ignores "dist/index.js" and "src/dist/util.js")
|
|
131
|
+
* - Patterns with / are relative to root
|
|
132
|
+
* - Negation (!) lines are skipped (not yet supported)
|
|
133
|
+
*/
|
|
134
|
+
|
|
135
|
+
export async function readMikkIgnore(projectRoot: string): Promise<string[]> {
|
|
136
|
+
const ignorePath = path.join(projectRoot, '.mikkignore')
|
|
137
|
+
try {
|
|
138
|
+
const content = await fs.readFile(ignorePath, 'utf-8')
|
|
139
|
+
return parseMikkIgnore(content)
|
|
140
|
+
} catch {
|
|
141
|
+
return [] // no .mikkignore — that's fine
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Parse .mikkignore content into fast-glob ignore patterns (exported for testing) */
|
|
146
|
+
export function parseMikkIgnore(content: string): string[] {
|
|
147
|
+
const patterns: string[] = []
|
|
148
|
+
for (const raw of content.split('\n')) {
|
|
149
|
+
const line = raw.trim()
|
|
150
|
+
if (!line || line.startsWith('#')) continue
|
|
151
|
+
if (line.startsWith('!')) continue // negations not yet supported
|
|
152
|
+
|
|
153
|
+
const isDir = line.endsWith('/')
|
|
154
|
+
// If pattern has no slash (ignoring trailing slash), match anywhere → prepend **/
|
|
155
|
+
const stripped = isDir ? line.slice(0, -1) : line
|
|
156
|
+
const hasSlash = stripped.includes('/')
|
|
157
|
+
|
|
158
|
+
if (!hasSlash) {
|
|
159
|
+
if (isDir) {
|
|
160
|
+
// e.g. "dist/" → "**/{dist}/**" — ignore the directory and everything within it
|
|
161
|
+
patterns.push(`**/${stripped}/**`)
|
|
162
|
+
} else {
|
|
163
|
+
// e.g. "*.svg" → "**/*.svg"
|
|
164
|
+
patterns.push(`**/${line}`)
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
if (isDir) {
|
|
168
|
+
// e.g. "packages/*/tests/" → "packages/*/tests/**"
|
|
169
|
+
patterns.push(`${stripped}/**`)
|
|
170
|
+
} else {
|
|
171
|
+
// e.g. "components/ui/**" — relative to root, already valid
|
|
172
|
+
patterns.push(line)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return patterns
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Discover structural / schema / config files that help an AI agent understand
|
|
181
|
+
* the project's data models, API definitions, route structure, and config.
|
|
182
|
+
*
|
|
183
|
+
* This is technology-agnostic: it works for Prisma, Drizzle, GraphQL, SQL,
|
|
184
|
+
* Protobuf, Docker, OpenAPI, and more — anything with a well-known file pattern.
|
|
185
|
+
*/
|
|
186
|
+
export async function discoverContextFiles(projectRoot: string): Promise<ContextFile[]> {
|
|
187
|
+
const mikkIgnore = await readMikkIgnore(projectRoot)
|
|
188
|
+
const files = await fg(CONTEXT_FILE_PATTERNS, {
|
|
189
|
+
cwd: projectRoot,
|
|
190
|
+
ignore: [...CONTEXT_FILE_IGNORE, ...mikkIgnore],
|
|
191
|
+
absolute: false,
|
|
192
|
+
onlyFiles: true,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const normalised = files.map(f => f.replace(/\\/g, '/'))
|
|
196
|
+
|
|
197
|
+
// Deduplicate — some patterns overlap (e.g. models/*.ts also matched by source discovery)
|
|
198
|
+
const unique = [...new Set(normalised)]
|
|
199
|
+
|
|
200
|
+
const results: ContextFile[] = []
|
|
201
|
+
|
|
202
|
+
for (const relPath of unique) {
|
|
203
|
+
const absPath = path.join(projectRoot, relPath)
|
|
204
|
+
try {
|
|
205
|
+
const stat = await fs.stat(absPath)
|
|
206
|
+
if (stat.size > MAX_CONTEXT_FILE_SIZE) continue // skip huge files
|
|
207
|
+
if (stat.size === 0) continue
|
|
208
|
+
|
|
209
|
+
const content = await fs.readFile(absPath, 'utf-8')
|
|
210
|
+
const type = inferContextFileType(relPath)
|
|
211
|
+
|
|
212
|
+
results.push({ path: relPath, content, type, size: stat.size })
|
|
213
|
+
} catch {
|
|
214
|
+
// File unreadable — skip
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Sort: schemas/models first, then types, routes, config
|
|
219
|
+
const priority: Record<ContextFileType, number> = {
|
|
220
|
+
schema: 0,
|
|
221
|
+
model: 1,
|
|
222
|
+
types: 2,
|
|
223
|
+
'api-spec': 3,
|
|
224
|
+
routes: 4,
|
|
225
|
+
migration: 5,
|
|
226
|
+
docker: 6,
|
|
227
|
+
config: 7,
|
|
228
|
+
}
|
|
229
|
+
results.sort((a, b) => priority[a.type] - priority[b.type])
|
|
230
|
+
|
|
231
|
+
// If we have a schema file (e.g. prisma/schema.prisma), the migrations
|
|
232
|
+
// are redundant — they represent historical deltas, not the current state.
|
|
233
|
+
// Including them wastes AI tokens and can be actively misleading.
|
|
234
|
+
const hasSchema = results.some(f => f.type === 'schema')
|
|
235
|
+
if (hasSchema) {
|
|
236
|
+
return results.filter(f => f.type !== 'migration')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return results
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Infer the context file's category from its path */
|
|
243
|
+
function inferContextFileType(filePath: string): ContextFileType {
|
|
244
|
+
const lower = filePath.toLowerCase()
|
|
245
|
+
// Schema files — multi-language
|
|
246
|
+
if (lower.includes('prisma/schema') || lower.endsWith('.prisma')) return 'schema'
|
|
247
|
+
if (lower.includes('drizzle/') || lower.includes('.schema.')) return 'schema'
|
|
248
|
+
if (lower.endsWith('.graphql') || lower.endsWith('.gql')) return 'schema'
|
|
249
|
+
if (lower.endsWith('.avsc') || lower.endsWith('.thrift')) return 'schema'
|
|
250
|
+
if (lower.endsWith('db/schema.rb')) return 'schema'
|
|
251
|
+
if (lower.endsWith('schema.rs')) return 'schema'
|
|
252
|
+
if (lower.endsWith('.proto')) return 'api-spec'
|
|
253
|
+
if (lower.includes('openapi') || lower.includes('swagger')) return 'api-spec'
|
|
254
|
+
// Migrations — multi-language
|
|
255
|
+
if (lower.endsWith('.sql') && lower.includes('migration')) return 'migration'
|
|
256
|
+
if (lower.includes('db/migrate/')) return 'migration'
|
|
257
|
+
if (lower.includes('alembic/')) return 'migration'
|
|
258
|
+
if (lower.endsWith('.sql')) return 'schema'
|
|
259
|
+
// Models — any language
|
|
260
|
+
if (lower.includes('/models/') || lower.includes('/model/')) return 'model'
|
|
261
|
+
if (lower.endsWith('.model.ts') || lower.endsWith('.model.js') || lower.endsWith('.model.go')) return 'model'
|
|
262
|
+
if (lower.endsWith('models.py') || lower.endsWith('serializers.py') || lower.endsWith('schemas.py')) return 'model'
|
|
263
|
+
if (lower.includes('/entity/') || lower.includes('/dto/') || lower.includes('/entities/')) return 'model'
|
|
264
|
+
if (lower.endsWith('_model.go') || lower.endsWith('models.rs')) return 'model'
|
|
265
|
+
// Types / Interfaces
|
|
266
|
+
if (lower.includes('/types/') || lower.startsWith('types/') || lower.endsWith('/types.ts') || lower.endsWith('/types.js')) return 'types'
|
|
267
|
+
if (lower.includes('/interfaces/') || lower.startsWith('interfaces/')) return 'types'
|
|
268
|
+
// Routes
|
|
269
|
+
if (lower.includes('/routes/') || lower.includes('router.')) return 'routes'
|
|
270
|
+
// Docker
|
|
271
|
+
if (lower.includes('docker') || lower.includes('dockerfile')) return 'docker'
|
|
272
|
+
// Config
|
|
273
|
+
if (lower.includes('.env')) return 'config'
|
|
274
|
+
return 'config'
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Recognised project language */
|
|
278
|
+
export type ProjectLanguage = 'typescript' | 'javascript' | 'python' | 'go' | 'rust' | 'java' | 'ruby' | 'php' | 'csharp' | 'unknown'
|
|
279
|
+
|
|
280
|
+
/** Auto-detect the project's primary language from manifest files */
|
|
281
|
+
export async function detectProjectLanguage(projectRoot: string): Promise<ProjectLanguage> {
|
|
282
|
+
const exists = async (name: string) => {
|
|
283
|
+
try { await fs.access(path.join(projectRoot, name)); return true } catch { return false }
|
|
284
|
+
}
|
|
285
|
+
const hasGlob = async (pattern: string) => {
|
|
286
|
+
const matches = await fg(pattern, { cwd: projectRoot, onlyFiles: true, deep: 1 })
|
|
287
|
+
return matches.length > 0
|
|
288
|
+
}
|
|
289
|
+
// Check in priority order — most specific first
|
|
290
|
+
if (await exists('tsconfig.json') || await hasGlob('tsconfig.*.json')) return 'typescript'
|
|
291
|
+
if (await exists('Cargo.toml')) return 'rust'
|
|
292
|
+
if (await exists('go.mod')) return 'go'
|
|
293
|
+
if (await exists('pyproject.toml') || await exists('setup.py') || await exists('requirements.txt')) return 'python'
|
|
294
|
+
if (await exists('Gemfile')) return 'ruby'
|
|
295
|
+
if (await exists('pom.xml') || await exists('build.gradle') || await exists('build.gradle.kts')) return 'java'
|
|
296
|
+
if (await exists('composer.json')) return 'php'
|
|
297
|
+
if (await hasGlob('*.csproj') || await hasGlob('*.sln')) return 'csharp'
|
|
298
|
+
if (await exists('package.json')) return 'javascript'
|
|
299
|
+
return 'unknown'
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Get source file glob patterns for a given language */
|
|
303
|
+
export function getDiscoveryPatterns(language: ProjectLanguage): { patterns: string[], ignore: string[] } {
|
|
304
|
+
const commonIgnore = [
|
|
305
|
+
'**/.mikk/**', '**/.git/**', '**/coverage/**', '**/build/**',
|
|
306
|
+
]
|
|
307
|
+
switch (language) {
|
|
308
|
+
case 'typescript':
|
|
309
|
+
return {
|
|
310
|
+
patterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
311
|
+
ignore: [...commonIgnore, '**/node_modules/**', '**/dist/**', '**/.next/**', '**/.nuxt/**', '**/.svelte-kit/**', '**/*.d.ts', '**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}'],
|
|
312
|
+
}
|
|
313
|
+
case 'javascript':
|
|
314
|
+
return {
|
|
315
|
+
patterns: ['**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs', '**/*.ts', '**/*.tsx'],
|
|
316
|
+
ignore: [...commonIgnore, '**/node_modules/**', '**/dist/**', '**/.next/**', '**/*.d.ts', '**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}'],
|
|
317
|
+
}
|
|
318
|
+
case 'python':
|
|
319
|
+
return {
|
|
320
|
+
patterns: ['**/*.py'],
|
|
321
|
+
ignore: [...commonIgnore, '**/__pycache__/**', '**/venv/**', '**/.venv/**', '**/.tox/**', '**/test_*.py', '**/*_test.py'],
|
|
322
|
+
}
|
|
323
|
+
case 'go':
|
|
324
|
+
return {
|
|
325
|
+
patterns: ['**/*.go'],
|
|
326
|
+
ignore: [...commonIgnore, '**/vendor/**', '**/*_test.go'],
|
|
327
|
+
}
|
|
328
|
+
case 'rust':
|
|
329
|
+
return {
|
|
330
|
+
patterns: ['**/*.rs'],
|
|
331
|
+
ignore: [...commonIgnore, '**/target/**'],
|
|
332
|
+
}
|
|
333
|
+
case 'java':
|
|
334
|
+
return {
|
|
335
|
+
patterns: ['**/*.java', '**/*.kt'],
|
|
336
|
+
ignore: [...commonIgnore, '**/target/**', '**/.gradle/**', '**/Test*.java', '**/*Test.java'],
|
|
337
|
+
}
|
|
338
|
+
case 'ruby':
|
|
339
|
+
return {
|
|
340
|
+
patterns: ['**/*.rb'],
|
|
341
|
+
ignore: [...commonIgnore, '**/vendor/**', '**/*_spec.rb', '**/spec/**'],
|
|
342
|
+
}
|
|
343
|
+
case 'php':
|
|
344
|
+
return {
|
|
345
|
+
patterns: ['**/*.php'],
|
|
346
|
+
ignore: [...commonIgnore, '**/vendor/**', '**/*Test.php'],
|
|
347
|
+
}
|
|
348
|
+
case 'csharp':
|
|
349
|
+
return {
|
|
350
|
+
patterns: ['**/*.cs'],
|
|
351
|
+
ignore: [...commonIgnore, '**/bin/**', '**/obj/**'],
|
|
352
|
+
}
|
|
353
|
+
default:
|
|
354
|
+
// Fallback: discover JS/TS (most common)
|
|
355
|
+
return {
|
|
356
|
+
patterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
357
|
+
ignore: [...commonIgnore, '**/node_modules/**', '**/dist/**', '**/*.d.ts'],
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
5
362
|
/**
|
|
6
363
|
* Discover all source files in a project directory.
|
|
7
|
-
* Respects common ignore patterns
|
|
364
|
+
* Respects common ignore patterns and supports multiple languages.
|
|
8
365
|
*/
|
|
9
366
|
export async function discoverFiles(
|
|
10
367
|
projectRoot: string,
|
|
11
|
-
patterns: string[] = ['**/*.ts', '**/*.tsx'],
|
|
12
|
-
ignore: string[] = ['**/node_modules/**', '**/dist/**', '**/.mikk/**', '**/coverage/**', '**/*.d.ts', '**/*.test.ts', '**/*.spec.ts']
|
|
368
|
+
patterns: string[] = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
369
|
+
ignore: string[] = ['**/node_modules/**', '**/dist/**', '**/.mikk/**', '**/coverage/**', '**/*.d.ts', '**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}']
|
|
13
370
|
): Promise<string[]> {
|
|
371
|
+
const mikkIgnore = await readMikkIgnore(projectRoot)
|
|
14
372
|
const files = await fg(patterns, {
|
|
15
373
|
cwd: projectRoot,
|
|
16
|
-
ignore,
|
|
374
|
+
ignore: [...ignore, ...mikkIgnore],
|
|
17
375
|
absolute: false,
|
|
18
376
|
onlyFiles: true,
|
|
19
377
|
})
|
|
@@ -73,3 +431,227 @@ export async function setupMikkDirectory(projectRoot: string): Promise<void> {
|
|
|
73
431
|
await fs.writeFile(impactKeep, '', 'utf-8')
|
|
74
432
|
}
|
|
75
433
|
}
|
|
434
|
+
|
|
435
|
+
// ─── .mikkignore auto-generation ────────────────────────────────────
|
|
436
|
+
|
|
437
|
+
/** Default ignore patterns shared across all languages */
|
|
438
|
+
const COMMON_IGNORE_PATTERNS = [
|
|
439
|
+
'# Build outputs',
|
|
440
|
+
'dist/',
|
|
441
|
+
'build/',
|
|
442
|
+
'out/',
|
|
443
|
+
'coverage/',
|
|
444
|
+
'',
|
|
445
|
+
'# Mikk internal',
|
|
446
|
+
'.mikk/',
|
|
447
|
+
'',
|
|
448
|
+
'# IDE / OS',
|
|
449
|
+
'.idea/',
|
|
450
|
+
'.vscode/',
|
|
451
|
+
'*.log',
|
|
452
|
+
'',
|
|
453
|
+
]
|
|
454
|
+
|
|
455
|
+
/** Language-specific ignore templates */
|
|
456
|
+
const LANGUAGE_IGNORE_TEMPLATES: Record<ProjectLanguage, string[]> = {
|
|
457
|
+
typescript: [
|
|
458
|
+
'# Test files',
|
|
459
|
+
'**/*.test.ts',
|
|
460
|
+
'**/*.test.tsx',
|
|
461
|
+
'**/*.spec.ts',
|
|
462
|
+
'**/*.spec.tsx',
|
|
463
|
+
'__tests__/',
|
|
464
|
+
'**/tests/fixtures/',
|
|
465
|
+
'**/test-utils/',
|
|
466
|
+
'',
|
|
467
|
+
'# Generated / declaration files',
|
|
468
|
+
'*.d.ts',
|
|
469
|
+
'',
|
|
470
|
+
'# Node.js',
|
|
471
|
+
'node_modules/',
|
|
472
|
+
'.next/',
|
|
473
|
+
'.nuxt/',
|
|
474
|
+
'.svelte-kit/',
|
|
475
|
+
'.astro/',
|
|
476
|
+
'',
|
|
477
|
+
],
|
|
478
|
+
javascript: [
|
|
479
|
+
'# Test files',
|
|
480
|
+
'**/*.test.js',
|
|
481
|
+
'**/*.test.jsx',
|
|
482
|
+
'**/*.spec.js',
|
|
483
|
+
'**/*.spec.jsx',
|
|
484
|
+
'__tests__/',
|
|
485
|
+
'**/tests/fixtures/',
|
|
486
|
+
'**/test-utils/',
|
|
487
|
+
'',
|
|
488
|
+
'# Generated / declaration files',
|
|
489
|
+
'*.d.ts',
|
|
490
|
+
'',
|
|
491
|
+
'# Node.js',
|
|
492
|
+
'node_modules/',
|
|
493
|
+
'.next/',
|
|
494
|
+
'',
|
|
495
|
+
],
|
|
496
|
+
python: [
|
|
497
|
+
'# Test files',
|
|
498
|
+
'test_*.py',
|
|
499
|
+
'*_test.py',
|
|
500
|
+
'conftest.py',
|
|
501
|
+
'tests/',
|
|
502
|
+
'**/tests/fixtures/',
|
|
503
|
+
'',
|
|
504
|
+
'# Python artifacts',
|
|
505
|
+
'__pycache__/',
|
|
506
|
+
'*.pyc',
|
|
507
|
+
'venv/',
|
|
508
|
+
'.venv/',
|
|
509
|
+
'.tox/',
|
|
510
|
+
'*.egg-info/',
|
|
511
|
+
'',
|
|
512
|
+
],
|
|
513
|
+
go: [
|
|
514
|
+
'# Test files',
|
|
515
|
+
'*_test.go',
|
|
516
|
+
'testdata/',
|
|
517
|
+
'',
|
|
518
|
+
'# Go artifacts',
|
|
519
|
+
'vendor/',
|
|
520
|
+
'',
|
|
521
|
+
],
|
|
522
|
+
rust: [
|
|
523
|
+
'# Test files (inline tests are kept — only test binaries excluded)',
|
|
524
|
+
'target/',
|
|
525
|
+
'tests/fixtures/',
|
|
526
|
+
'',
|
|
527
|
+
],
|
|
528
|
+
java: [
|
|
529
|
+
'# Test files',
|
|
530
|
+
'**/src/test/**',
|
|
531
|
+
'Test*.java',
|
|
532
|
+
'*Test.java',
|
|
533
|
+
'*Tests.java',
|
|
534
|
+
'',
|
|
535
|
+
'# Build artifacts',
|
|
536
|
+
'target/',
|
|
537
|
+
'.gradle/',
|
|
538
|
+
'gradle/',
|
|
539
|
+
'',
|
|
540
|
+
],
|
|
541
|
+
ruby: [
|
|
542
|
+
'# Test files',
|
|
543
|
+
'*_spec.rb',
|
|
544
|
+
'spec/',
|
|
545
|
+
'test/',
|
|
546
|
+
'',
|
|
547
|
+
'# Ruby artifacts',
|
|
548
|
+
'vendor/',
|
|
549
|
+
'',
|
|
550
|
+
],
|
|
551
|
+
php: [
|
|
552
|
+
'# Test files',
|
|
553
|
+
'*Test.php',
|
|
554
|
+
'tests/',
|
|
555
|
+
'',
|
|
556
|
+
'# PHP artifacts',
|
|
557
|
+
'vendor/',
|
|
558
|
+
'',
|
|
559
|
+
],
|
|
560
|
+
csharp: [
|
|
561
|
+
'# Test files',
|
|
562
|
+
'*.Tests/',
|
|
563
|
+
'*.Test/',
|
|
564
|
+
'**/Tests/**',
|
|
565
|
+
'',
|
|
566
|
+
'# Build artifacts',
|
|
567
|
+
'bin/',
|
|
568
|
+
'obj/',
|
|
569
|
+
'',
|
|
570
|
+
],
|
|
571
|
+
unknown: [
|
|
572
|
+
'# Test files (add your patterns here)',
|
|
573
|
+
'tests/',
|
|
574
|
+
'test/',
|
|
575
|
+
'__tests__/',
|
|
576
|
+
'',
|
|
577
|
+
],
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Generate a .mikkignore file with smart defaults for the detected language.
|
|
582
|
+
* Only creates the file if it doesn't already exist.
|
|
583
|
+
* Returns true if a file was created, false if one already exists.
|
|
584
|
+
*/
|
|
585
|
+
export async function generateMikkIgnore(projectRoot: string, language: ProjectLanguage): Promise<boolean> {
|
|
586
|
+
const ignorePath = path.join(projectRoot, '.mikkignore')
|
|
587
|
+
|
|
588
|
+
// Don't overwrite an existing .mikkignore
|
|
589
|
+
if (await fileExists(ignorePath)) return false
|
|
590
|
+
|
|
591
|
+
const lines: string[] = [
|
|
592
|
+
'# .mikkignore — files/directories Mikk should skip during analysis',
|
|
593
|
+
'# Syntax: gitignore-style patterns. Lines starting with # are comments.',
|
|
594
|
+
'# Paths without / match anywhere. Paths with / are relative to project root.',
|
|
595
|
+
'',
|
|
596
|
+
...COMMON_IGNORE_PATTERNS,
|
|
597
|
+
...LANGUAGE_IGNORE_TEMPLATES[language],
|
|
598
|
+
]
|
|
599
|
+
|
|
600
|
+
// Monorepo detection: if there are workspace definitions, add common
|
|
601
|
+
// monorepo patterns (e.g. packages/*/tests/, apps/*/tests/)
|
|
602
|
+
try {
|
|
603
|
+
const pkgRaw = await fs.readFile(path.join(projectRoot, 'package.json'), 'utf-8')
|
|
604
|
+
const pkg = JSON.parse(pkgRaw)
|
|
605
|
+
const workspaces: string[] | undefined = Array.isArray(pkg.workspaces)
|
|
606
|
+
? pkg.workspaces
|
|
607
|
+
: pkg.workspaces?.packages
|
|
608
|
+
|
|
609
|
+
if (workspaces && workspaces.length > 0) {
|
|
610
|
+
lines.push('# Monorepo — test/fixture directories across all packages')
|
|
611
|
+
for (const ws of workspaces) {
|
|
612
|
+
// ws is like "packages/*" or "apps/*"
|
|
613
|
+
const base = ws.replace(/\/?\*$/, '')
|
|
614
|
+
lines.push(`${base}/*/tests/`)
|
|
615
|
+
lines.push(`${base}/*/__tests__/`)
|
|
616
|
+
lines.push(`${base}/*/test/`)
|
|
617
|
+
}
|
|
618
|
+
lines.push('')
|
|
619
|
+
}
|
|
620
|
+
} catch {
|
|
621
|
+
// No package.json or not JSON — skip monorepo detection
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Turbo / pnpm workspace detection
|
|
625
|
+
try {
|
|
626
|
+
const turboRaw = await fs.readFile(path.join(projectRoot, 'turbo.json'), 'utf-8')
|
|
627
|
+
// turbo.json exists — likely a monorepo already handled above
|
|
628
|
+
void turboRaw
|
|
629
|
+
} catch {
|
|
630
|
+
// not a turbo project
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// pnpm-workspace.yaml detection
|
|
634
|
+
try {
|
|
635
|
+
const pnpmWs = await fs.readFile(path.join(projectRoot, 'pnpm-workspace.yaml'), 'utf-8')
|
|
636
|
+
// Extract package paths from "packages:" section
|
|
637
|
+
const packageLines = pnpmWs.split('\n')
|
|
638
|
+
.filter(l => l.trim().startsWith('-'))
|
|
639
|
+
.map(l => l.replace(/^\s*-\s*['"]?/, '').replace(/['"]?\s*$/, '').trim())
|
|
640
|
+
|
|
641
|
+
if (packageLines.length > 0 && !lines.some(l => l.includes('Monorepo'))) {
|
|
642
|
+
lines.push('# Monorepo (pnpm) — test/fixture directories across all packages')
|
|
643
|
+
for (const ws of packageLines) {
|
|
644
|
+
const base = ws.replace(/\/?\*$/, '')
|
|
645
|
+
lines.push(`${base}/*/tests/`)
|
|
646
|
+
lines.push(`${base}/*/__tests__/`)
|
|
647
|
+
lines.push(`${base}/*/test/`)
|
|
648
|
+
}
|
|
649
|
+
lines.push('')
|
|
650
|
+
}
|
|
651
|
+
} catch {
|
|
652
|
+
// no pnpm-workspace.yaml
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
await fs.writeFile(ignorePath, lines.join('\n'), 'utf-8')
|
|
656
|
+
return true
|
|
657
|
+
}
|