@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,41 +1,41 @@
|
|
|
1
|
-
import { BaseParser } from '../base-parser.js'
|
|
2
|
-
import { TypeScriptExtractor } from './ts-extractor.js'
|
|
3
|
-
import { TypeScriptResolver } from './ts-resolver.js'
|
|
4
|
-
import { hashContent } from '../../hash/file-hasher.js'
|
|
5
|
-
import type { ParsedFile } from '../types.js'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* TypeScript parser — uses TS Compiler API to parse .ts/.tsx files
|
|
9
|
-
* and extract structured data (functions, classes, imports, exports).
|
|
10
|
-
*/
|
|
11
|
-
export class TypeScriptParser extends BaseParser {
|
|
12
|
-
/** Parse a single TypeScript file */
|
|
13
|
-
parse(filePath: string, content: string): ParsedFile {
|
|
14
|
-
const extractor = new TypeScriptExtractor(filePath, content)
|
|
15
|
-
return {
|
|
16
|
-
path: filePath,
|
|
17
|
-
language: 'typescript',
|
|
18
|
-
functions: extractor.extractFunctions(),
|
|
19
|
-
classes: extractor.extractClasses(),
|
|
20
|
-
generics: extractor.extractGenerics(),
|
|
21
|
-
imports: extractor.extractImports(),
|
|
22
|
-
exports: extractor.extractExports(),
|
|
23
|
-
hash: hashContent(content),
|
|
24
|
-
parsedAt: Date.now(),
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Resolve all import paths in parsed files to absolute project paths */
|
|
29
|
-
resolveImports(files: ParsedFile[], projectRoot: string): ParsedFile[] {
|
|
30
|
-
const resolver = new TypeScriptResolver(projectRoot)
|
|
31
|
-
const allFilePaths = files.map(f => f.path)
|
|
32
|
-
return files.map(file => ({
|
|
33
|
-
...file,
|
|
34
|
-
imports: file.imports.map(imp => resolver.resolve(imp, file.path, allFilePaths)),
|
|
35
|
-
}))
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
getSupportedExtensions(): string[] {
|
|
39
|
-
return ['.ts', '.tsx']
|
|
40
|
-
}
|
|
41
|
-
}
|
|
1
|
+
import { BaseParser } from '../base-parser.js'
|
|
2
|
+
import { TypeScriptExtractor } from './ts-extractor.js'
|
|
3
|
+
import { TypeScriptResolver } from './ts-resolver.js'
|
|
4
|
+
import { hashContent } from '../../hash/file-hasher.js'
|
|
5
|
+
import type { ParsedFile } from '../types.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* TypeScript parser — uses TS Compiler API to parse .ts/.tsx files
|
|
9
|
+
* and extract structured data (functions, classes, imports, exports).
|
|
10
|
+
*/
|
|
11
|
+
export class TypeScriptParser extends BaseParser {
|
|
12
|
+
/** Parse a single TypeScript file */
|
|
13
|
+
parse(filePath: string, content: string): ParsedFile {
|
|
14
|
+
const extractor = new TypeScriptExtractor(filePath, content)
|
|
15
|
+
return {
|
|
16
|
+
path: filePath,
|
|
17
|
+
language: 'typescript',
|
|
18
|
+
functions: extractor.extractFunctions(),
|
|
19
|
+
classes: extractor.extractClasses(),
|
|
20
|
+
generics: extractor.extractGenerics(),
|
|
21
|
+
imports: extractor.extractImports(),
|
|
22
|
+
exports: extractor.extractExports(),
|
|
23
|
+
hash: hashContent(content),
|
|
24
|
+
parsedAt: Date.now(),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Resolve all import paths in parsed files to absolute project paths */
|
|
29
|
+
resolveImports(files: ParsedFile[], projectRoot: string): ParsedFile[] {
|
|
30
|
+
const resolver = new TypeScriptResolver(projectRoot)
|
|
31
|
+
const allFilePaths = files.map(f => f.path)
|
|
32
|
+
return files.map(file => ({
|
|
33
|
+
...file,
|
|
34
|
+
imports: file.imports.map(imp => resolver.resolve(imp, file.path, allFilePaths)),
|
|
35
|
+
}))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getSupportedExtensions(): string[] {
|
|
39
|
+
return ['.ts', '.tsx']
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
import * as path from 'node:path'
|
|
2
|
-
import type { ParsedImport } from '../types.js'
|
|
3
|
-
|
|
4
|
-
interface TSConfigPaths {
|
|
5
|
-
[alias: string]: string[]
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Resolves TypeScript import paths to absolute project-relative paths.
|
|
10
|
-
* Handles: relative imports, path aliases, index files, extension inference.
|
|
11
|
-
*/
|
|
12
|
-
export class TypeScriptResolver {
|
|
13
|
-
private aliases: TSConfigPaths
|
|
14
|
-
|
|
15
|
-
constructor(
|
|
16
|
-
private projectRoot: string,
|
|
17
|
-
tsConfigPaths?: TSConfigPaths
|
|
18
|
-
) {
|
|
19
|
-
this.aliases = tsConfigPaths || {}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Resolve a single import relative to the importing file */
|
|
23
|
-
resolve(imp: ParsedImport, fromFile: string, allProjectFiles: string[] = []): ParsedImport {
|
|
24
|
-
// Skip external packages (no relative path prefix, no alias match)
|
|
25
|
-
if (!imp.source.startsWith('.') && !imp.source.startsWith('/') && !this.matchesAlias(imp.source)) {
|
|
26
|
-
return { ...imp, resolvedPath: '' }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const resolved = this.resolvePath(imp.source, fromFile, allProjectFiles)
|
|
30
|
-
return { ...imp, resolvedPath: resolved }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private resolvePath(source: string, fromFile: string, allProjectFiles: string[]): string {
|
|
34
|
-
let resolvedSource = source
|
|
35
|
-
|
|
36
|
-
// 1. Handle path aliases: @/utils/jwt → src/utils/jwt
|
|
37
|
-
for (const [alias, targets] of Object.entries(this.aliases)) {
|
|
38
|
-
const aliasPrefix = alias.replace('/*', '')
|
|
39
|
-
if (source.startsWith(aliasPrefix)) {
|
|
40
|
-
const suffix = source.slice(aliasPrefix.length)
|
|
41
|
-
const target = targets[0].replace('/*', '')
|
|
42
|
-
resolvedSource = target + suffix
|
|
43
|
-
break
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// 2. Handle relative paths
|
|
48
|
-
let resolved: string
|
|
49
|
-
if (resolvedSource.startsWith('.')) {
|
|
50
|
-
const fromDir = path.dirname(fromFile)
|
|
51
|
-
resolved = path.posix.normalize(path.posix.join(fromDir, resolvedSource))
|
|
52
|
-
} else {
|
|
53
|
-
resolved = resolvedSource
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Normalize to posix
|
|
57
|
-
resolved = resolved.replace(/\\/g, '/')
|
|
58
|
-
|
|
59
|
-
// 3. Try to find exact match with extensions
|
|
60
|
-
const extensions = ['.ts', '.tsx', '/index.ts', '/index.tsx']
|
|
61
|
-
|
|
62
|
-
// If the path already has an extension, return it
|
|
63
|
-
if (resolved.endsWith('.ts') || resolved.endsWith('.tsx')) {
|
|
64
|
-
return resolved
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Try adding extensions to find matching file
|
|
68
|
-
for (const ext of extensions) {
|
|
69
|
-
const candidate = resolved + ext
|
|
70
|
-
if (allProjectFiles.length === 0 || allProjectFiles.includes(candidate)) {
|
|
71
|
-
return candidate
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Fallback: just add .ts
|
|
76
|
-
return resolved + '.ts'
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private matchesAlias(source: string): boolean {
|
|
80
|
-
for (const alias of Object.keys(this.aliases)) {
|
|
81
|
-
const prefix = alias.replace('/*', '')
|
|
82
|
-
if (source.startsWith(prefix)) return true
|
|
83
|
-
}
|
|
84
|
-
return false
|
|
85
|
-
}
|
|
86
|
-
}
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import type { ParsedImport } from '../types.js'
|
|
3
|
+
|
|
4
|
+
interface TSConfigPaths {
|
|
5
|
+
[alias: string]: string[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves TypeScript import paths to absolute project-relative paths.
|
|
10
|
+
* Handles: relative imports, path aliases, index files, extension inference.
|
|
11
|
+
*/
|
|
12
|
+
export class TypeScriptResolver {
|
|
13
|
+
private aliases: TSConfigPaths
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
private projectRoot: string,
|
|
17
|
+
tsConfigPaths?: TSConfigPaths
|
|
18
|
+
) {
|
|
19
|
+
this.aliases = tsConfigPaths || {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Resolve a single import relative to the importing file */
|
|
23
|
+
resolve(imp: ParsedImport, fromFile: string, allProjectFiles: string[] = []): ParsedImport {
|
|
24
|
+
// Skip external packages (no relative path prefix, no alias match)
|
|
25
|
+
if (!imp.source.startsWith('.') && !imp.source.startsWith('/') && !this.matchesAlias(imp.source)) {
|
|
26
|
+
return { ...imp, resolvedPath: '' }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const resolved = this.resolvePath(imp.source, fromFile, allProjectFiles)
|
|
30
|
+
return { ...imp, resolvedPath: resolved }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private resolvePath(source: string, fromFile: string, allProjectFiles: string[]): string {
|
|
34
|
+
let resolvedSource = source
|
|
35
|
+
|
|
36
|
+
// 1. Handle path aliases: @/utils/jwt → src/utils/jwt
|
|
37
|
+
for (const [alias, targets] of Object.entries(this.aliases)) {
|
|
38
|
+
const aliasPrefix = alias.replace('/*', '')
|
|
39
|
+
if (source.startsWith(aliasPrefix)) {
|
|
40
|
+
const suffix = source.slice(aliasPrefix.length)
|
|
41
|
+
const target = targets[0].replace('/*', '')
|
|
42
|
+
resolvedSource = target + suffix
|
|
43
|
+
break
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. Handle relative paths
|
|
48
|
+
let resolved: string
|
|
49
|
+
if (resolvedSource.startsWith('.')) {
|
|
50
|
+
const fromDir = path.dirname(fromFile)
|
|
51
|
+
resolved = path.posix.normalize(path.posix.join(fromDir, resolvedSource))
|
|
52
|
+
} else {
|
|
53
|
+
resolved = resolvedSource
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Normalize to posix
|
|
57
|
+
resolved = resolved.replace(/\\/g, '/')
|
|
58
|
+
|
|
59
|
+
// 3. Try to find exact match with extensions
|
|
60
|
+
const extensions = ['.ts', '.tsx', '/index.ts', '/index.tsx']
|
|
61
|
+
|
|
62
|
+
// If the path already has an extension, return it
|
|
63
|
+
if (resolved.endsWith('.ts') || resolved.endsWith('.tsx')) {
|
|
64
|
+
return resolved
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Try adding extensions to find matching file
|
|
68
|
+
for (const ext of extensions) {
|
|
69
|
+
const candidate = resolved + ext
|
|
70
|
+
if (allProjectFiles.length === 0 || allProjectFiles.includes(candidate)) {
|
|
71
|
+
return candidate
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Fallback: just add .ts
|
|
76
|
+
return resolved + '.ts'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private matchesAlias(source: string): boolean {
|
|
80
|
+
for (const alias of Object.keys(this.aliases)) {
|
|
81
|
+
const prefix = alias.replace('/*', '')
|
|
82
|
+
if (source.startsWith(prefix)) return true
|
|
83
|
+
}
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/utils/errors.ts
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
export class MikkError extends Error {
|
|
2
|
-
constructor(message: string, public code: string) {
|
|
3
|
-
super(message)
|
|
4
|
-
this.name = 'MikkError'
|
|
5
|
-
}
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export class ParseError extends MikkError {
|
|
9
|
-
constructor(file: string, cause: string) {
|
|
10
|
-
super(`Failed to parse ${file}: ${cause}`, 'PARSE_ERROR')
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class ContractNotFoundError extends MikkError {
|
|
15
|
-
constructor(path: string) {
|
|
16
|
-
super(`No mikk.json found at ${path}. Run 'mikk init' first.`, 'CONTRACT_NOT_FOUND')
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class LockNotFoundError extends MikkError {
|
|
21
|
-
constructor() {
|
|
22
|
-
super(`No mikk.lock.json found. Run 'mikk analyze' first.`, 'LOCK_NOT_FOUND')
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class UnsupportedLanguageError extends MikkError {
|
|
27
|
-
constructor(ext: string) {
|
|
28
|
-
super(`Unsupported file extension: ${ext}`, 'UNSUPPORTED_LANGUAGE')
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class OverwritePermissionError extends MikkError {
|
|
33
|
-
constructor() {
|
|
34
|
-
super(`Overwrite mode is 'never'. Change to 'ask' or 'explicit' to allow updates.`, 'OVERWRITE_DENIED')
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export class SyncStateError extends MikkError {
|
|
39
|
-
constructor(status: string) {
|
|
40
|
-
super(`Mikk is in ${status} state. Run 'mikk analyze' to sync.`, 'SYNC_STATE_ERROR')
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
export class MikkError extends Error {
|
|
2
|
+
constructor(message: string, public code: string) {
|
|
3
|
+
super(message)
|
|
4
|
+
this.name = 'MikkError'
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class ParseError extends MikkError {
|
|
9
|
+
constructor(file: string, cause: string) {
|
|
10
|
+
super(`Failed to parse ${file}: ${cause}`, 'PARSE_ERROR')
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ContractNotFoundError extends MikkError {
|
|
15
|
+
constructor(path: string) {
|
|
16
|
+
super(`No mikk.json found at ${path}. Run 'mikk init' first.`, 'CONTRACT_NOT_FOUND')
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class LockNotFoundError extends MikkError {
|
|
21
|
+
constructor() {
|
|
22
|
+
super(`No mikk.lock.json found. Run 'mikk analyze' first.`, 'LOCK_NOT_FOUND')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class UnsupportedLanguageError extends MikkError {
|
|
27
|
+
constructor(ext: string) {
|
|
28
|
+
super(`Unsupported file extension: ${ext}`, 'UNSUPPORTED_LANGUAGE')
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class OverwritePermissionError extends MikkError {
|
|
33
|
+
constructor() {
|
|
34
|
+
super(`Overwrite mode is 'never'. Change to 'ask' or 'explicit' to allow updates.`, 'OVERWRITE_DENIED')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class SyncStateError extends MikkError {
|
|
39
|
+
constructor(status: string) {
|
|
40
|
+
super(`Mikk is in ${status} state. Run 'mikk analyze' to sync.`, 'SYNC_STATE_ERROR')
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/utils/fs.ts
CHANGED
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
import * as fs from 'node:fs/promises'
|
|
2
|
-
import * as path from 'node:path'
|
|
3
|
-
import fg from 'fast-glob'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Discover all source files in a project directory.
|
|
7
|
-
* Respects common ignore patterns (node_modules, dist, .mikk, etc.)
|
|
8
|
-
*/
|
|
9
|
-
export async function discoverFiles(
|
|
10
|
-
projectRoot: string,
|
|
11
|
-
patterns: string[] = ['**/*.ts', '**/*.tsx'],
|
|
12
|
-
ignore: string[] = ['**/node_modules/**', '**/dist/**', '**/.mikk/**', '**/coverage/**', '**/*.d.ts', '**/*.test.ts', '**/*.spec.ts']
|
|
13
|
-
): Promise<string[]> {
|
|
14
|
-
const files = await fg(patterns, {
|
|
15
|
-
cwd: projectRoot,
|
|
16
|
-
ignore,
|
|
17
|
-
absolute: false,
|
|
18
|
-
onlyFiles: true,
|
|
19
|
-
})
|
|
20
|
-
return files.map(f => f.replace(/\\/g, '/'))
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Reads a file and returns its content as a UTF-8 string.
|
|
25
|
-
*/
|
|
26
|
-
export async function readFileContent(filePath: string): Promise<string> {
|
|
27
|
-
return fs.readFile(filePath, 'utf-8')
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Writes content to a file, creating parent directories if needed.
|
|
32
|
-
*/
|
|
33
|
-
export async function writeFileContent(filePath: string, content: string): Promise<void> {
|
|
34
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
35
|
-
await fs.writeFile(filePath, content, 'utf-8')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Check if a file exists.
|
|
40
|
-
*/
|
|
41
|
-
export async function fileExists(filePath: string): Promise<boolean> {
|
|
42
|
-
try {
|
|
43
|
-
await fs.access(filePath)
|
|
44
|
-
return true
|
|
45
|
-
} catch {
|
|
46
|
-
return false
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Set up the .mikk directory structure in a project root.
|
|
52
|
-
*/
|
|
53
|
-
export async function setupMikkDirectory(projectRoot: string): Promise<void> {
|
|
54
|
-
const dirs = [
|
|
55
|
-
'.mikk',
|
|
56
|
-
'.mikk/fragments',
|
|
57
|
-
'.mikk/diagrams',
|
|
58
|
-
'.mikk/diagrams/modules',
|
|
59
|
-
'.mikk/diagrams/capsules',
|
|
60
|
-
'.mikk/diagrams/flows',
|
|
61
|
-
'.mikk/diagrams/impact',
|
|
62
|
-
'.mikk/diagrams/exposure',
|
|
63
|
-
'.mikk/intent',
|
|
64
|
-
'.mikk/cache',
|
|
65
|
-
]
|
|
66
|
-
for (const dir of dirs) {
|
|
67
|
-
await fs.mkdir(path.join(projectRoot, dir), { recursive: true })
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Create .gitkeep in impact dir
|
|
71
|
-
const impactKeep = path.join(projectRoot, '.mikk/diagrams/impact/.gitkeep')
|
|
72
|
-
if (!await fileExists(impactKeep)) {
|
|
73
|
-
await fs.writeFile(impactKeep, '', 'utf-8')
|
|
74
|
-
}
|
|
75
|
-
}
|
|
1
|
+
import * as fs from 'node:fs/promises'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
import fg from 'fast-glob'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Discover all source files in a project directory.
|
|
7
|
+
* Respects common ignore patterns (node_modules, dist, .mikk, etc.)
|
|
8
|
+
*/
|
|
9
|
+
export async function discoverFiles(
|
|
10
|
+
projectRoot: string,
|
|
11
|
+
patterns: string[] = ['**/*.ts', '**/*.tsx'],
|
|
12
|
+
ignore: string[] = ['**/node_modules/**', '**/dist/**', '**/.mikk/**', '**/coverage/**', '**/*.d.ts', '**/*.test.ts', '**/*.spec.ts']
|
|
13
|
+
): Promise<string[]> {
|
|
14
|
+
const files = await fg(patterns, {
|
|
15
|
+
cwd: projectRoot,
|
|
16
|
+
ignore,
|
|
17
|
+
absolute: false,
|
|
18
|
+
onlyFiles: true,
|
|
19
|
+
})
|
|
20
|
+
return files.map(f => f.replace(/\\/g, '/'))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reads a file and returns its content as a UTF-8 string.
|
|
25
|
+
*/
|
|
26
|
+
export async function readFileContent(filePath: string): Promise<string> {
|
|
27
|
+
return fs.readFile(filePath, 'utf-8')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Writes content to a file, creating parent directories if needed.
|
|
32
|
+
*/
|
|
33
|
+
export async function writeFileContent(filePath: string, content: string): Promise<void> {
|
|
34
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
35
|
+
await fs.writeFile(filePath, content, 'utf-8')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if a file exists.
|
|
40
|
+
*/
|
|
41
|
+
export async function fileExists(filePath: string): Promise<boolean> {
|
|
42
|
+
try {
|
|
43
|
+
await fs.access(filePath)
|
|
44
|
+
return true
|
|
45
|
+
} catch {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Set up the .mikk directory structure in a project root.
|
|
52
|
+
*/
|
|
53
|
+
export async function setupMikkDirectory(projectRoot: string): Promise<void> {
|
|
54
|
+
const dirs = [
|
|
55
|
+
'.mikk',
|
|
56
|
+
'.mikk/fragments',
|
|
57
|
+
'.mikk/diagrams',
|
|
58
|
+
'.mikk/diagrams/modules',
|
|
59
|
+
'.mikk/diagrams/capsules',
|
|
60
|
+
'.mikk/diagrams/flows',
|
|
61
|
+
'.mikk/diagrams/impact',
|
|
62
|
+
'.mikk/diagrams/exposure',
|
|
63
|
+
'.mikk/intent',
|
|
64
|
+
'.mikk/cache',
|
|
65
|
+
]
|
|
66
|
+
for (const dir of dirs) {
|
|
67
|
+
await fs.mkdir(path.join(projectRoot, dir), { recursive: true })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create .gitkeep in impact dir
|
|
71
|
+
const impactKeep = path.join(projectRoot, '.mikk/diagrams/impact/.gitkeep')
|
|
72
|
+
if (!await fileExists(impactKeep)) {
|
|
73
|
+
await fs.writeFile(impactKeep, '', 'utf-8')
|
|
74
|
+
}
|
|
75
|
+
}
|