@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/package.json
CHANGED
|
@@ -2,6 +2,31 @@ import type { MikkContract } from './schema.js'
|
|
|
2
2
|
import type { ModuleCluster } from '../graph/types.js'
|
|
3
3
|
import type { ParsedFile } from '../parser/types.js'
|
|
4
4
|
|
|
5
|
+
/** Common entry point filenames across ecosystems (without extensions) */
|
|
6
|
+
const ENTRY_BASENAMES = ['index', 'main', 'app', 'server', 'mod', 'lib', '__init__', 'manage', 'program', 'startup']
|
|
7
|
+
|
|
8
|
+
/** Infer the project language from the file extensions present */
|
|
9
|
+
function inferLanguageFromFiles(parsedFiles: ParsedFile[]): string {
|
|
10
|
+
const extCounts = new Map<string, number>()
|
|
11
|
+
for (const f of parsedFiles) {
|
|
12
|
+
const ext = f.path.split('.').pop()?.toLowerCase() || ''
|
|
13
|
+
extCounts.set(ext, (extCounts.get(ext) || 0) + 1)
|
|
14
|
+
}
|
|
15
|
+
// Determine dominant extension
|
|
16
|
+
let maxExt = 'ts'
|
|
17
|
+
let maxCount = 0
|
|
18
|
+
for (const [ext, count] of extCounts) {
|
|
19
|
+
if (count > maxCount) { maxExt = ext; maxCount = count }
|
|
20
|
+
}
|
|
21
|
+
const extToLang: Record<string, string> = {
|
|
22
|
+
ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript',
|
|
23
|
+
mjs: 'javascript', cjs: 'javascript', py: 'python', go: 'go',
|
|
24
|
+
rs: 'rust', java: 'java', kt: 'kotlin', rb: 'ruby', php: 'php',
|
|
25
|
+
cs: 'csharp', swift: 'swift', dart: 'dart', ex: 'elixir', exs: 'elixir',
|
|
26
|
+
}
|
|
27
|
+
return extToLang[maxExt] || maxExt
|
|
28
|
+
}
|
|
29
|
+
|
|
5
30
|
/**
|
|
6
31
|
* ContractGenerator — generates a mikk.json skeleton from graph analysis.
|
|
7
32
|
* Takes detected module clusters and produces a human-refinable contract.
|
|
@@ -11,32 +36,36 @@ export class ContractGenerator {
|
|
|
11
36
|
generateFromClusters(
|
|
12
37
|
clusters: ModuleCluster[],
|
|
13
38
|
parsedFiles: ParsedFile[],
|
|
14
|
-
projectName: string
|
|
39
|
+
projectName: string,
|
|
40
|
+
packageJsonDescription?: string
|
|
15
41
|
): MikkContract {
|
|
16
42
|
const modules = clusters.map(cluster => ({
|
|
17
43
|
id: cluster.id,
|
|
18
44
|
name: cluster.suggestedName,
|
|
19
|
-
description:
|
|
45
|
+
description: this.inferModuleDescription(cluster, parsedFiles),
|
|
20
46
|
intent: '',
|
|
21
47
|
paths: this.inferPaths(cluster.files),
|
|
22
48
|
entryFunctions: this.inferEntryFunctions(cluster, parsedFiles),
|
|
23
49
|
}))
|
|
24
50
|
|
|
25
|
-
// Detect entry points
|
|
51
|
+
// Detect entry points — language-agnostic basename matching
|
|
26
52
|
const entryPoints = parsedFiles
|
|
27
53
|
.filter(f => {
|
|
28
|
-
const basename = f.path.split('/').pop() || ''
|
|
29
|
-
return
|
|
54
|
+
const basename = (f.path.split('/').pop() || '').replace(/\.[^.]+$/, '')
|
|
55
|
+
return ENTRY_BASENAMES.includes(basename)
|
|
30
56
|
})
|
|
31
57
|
.map(f => f.path)
|
|
32
58
|
|
|
59
|
+
const detectedLanguage = inferLanguageFromFiles(parsedFiles)
|
|
60
|
+
const fallbackEntry = parsedFiles[0]?.path ?? 'src/index'
|
|
61
|
+
|
|
33
62
|
return {
|
|
34
63
|
version: '1.0.0',
|
|
35
64
|
project: {
|
|
36
65
|
name: projectName,
|
|
37
|
-
description: '',
|
|
38
|
-
language:
|
|
39
|
-
entryPoints: entryPoints.length > 0 ? entryPoints : [
|
|
66
|
+
description: packageJsonDescription || '',
|
|
67
|
+
language: detectedLanguage,
|
|
68
|
+
entryPoints: entryPoints.length > 0 ? entryPoints : [fallbackEntry],
|
|
40
69
|
},
|
|
41
70
|
declared: {
|
|
42
71
|
modules,
|
|
@@ -50,6 +79,56 @@ export class ContractGenerator {
|
|
|
50
79
|
}
|
|
51
80
|
}
|
|
52
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Infer a meaningful description for a module from its functions.
|
|
84
|
+
* Analyses function names, purposes, and patterns to produce
|
|
85
|
+
* something like "Handles user authentication and JWT verification"
|
|
86
|
+
* instead of "Contains 4 files with 12 functions".
|
|
87
|
+
*/
|
|
88
|
+
private inferModuleDescription(cluster: ModuleCluster, parsedFiles: ParsedFile[]): string {
|
|
89
|
+
const clusterFileSet = new Set(cluster.files)
|
|
90
|
+
const purposes: string[] = []
|
|
91
|
+
const fnNames: string[] = []
|
|
92
|
+
let hasExported = 0
|
|
93
|
+
let totalFunctions = 0
|
|
94
|
+
|
|
95
|
+
for (const file of parsedFiles) {
|
|
96
|
+
if (!clusterFileSet.has(file.path)) continue
|
|
97
|
+
for (const fn of file.functions) {
|
|
98
|
+
totalFunctions++
|
|
99
|
+
fnNames.push(fn.name)
|
|
100
|
+
if (fn.isExported) hasExported++
|
|
101
|
+
if (fn.purpose) purposes.push(fn.purpose)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// If we have good JSDoc purposes, summarise the top ones
|
|
106
|
+
if (purposes.length > 0) {
|
|
107
|
+
// Deduplicate and pick up to 3 unique purpose summaries
|
|
108
|
+
const unique = [...new Set(purposes)]
|
|
109
|
+
const short = unique.slice(0, 3).map(p => {
|
|
110
|
+
// Take first sentence, max 60 chars
|
|
111
|
+
const first = p.split(/[.!?]/)[0].trim()
|
|
112
|
+
return first.length > 60 ? first.slice(0, 57) + '...' : first
|
|
113
|
+
})
|
|
114
|
+
return short.join('; ')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fallback: describe by dominant verb patterns
|
|
118
|
+
const verbs = new Map<string, number>()
|
|
119
|
+
for (const name of fnNames) {
|
|
120
|
+
const first = name.replace(/([a-z])([A-Z])/g, '$1 $2').split(/[\s_-]/)[0].toLowerCase()
|
|
121
|
+
verbs.set(first, (verbs.get(first) || 0) + 1)
|
|
122
|
+
}
|
|
123
|
+
const sorted = [...verbs.entries()].sort((a, b) => b[1] - a[1])
|
|
124
|
+
if (sorted.length > 0) {
|
|
125
|
+
const top = sorted.slice(0, 3).map(([v]) => v)
|
|
126
|
+
return `primarily ${top.join(', ')} operations across ${cluster.files.length} files`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return `${cluster.files.length} files, ${totalFunctions} functions`
|
|
130
|
+
}
|
|
131
|
+
|
|
53
132
|
/** Infer path patterns from a list of files */
|
|
54
133
|
private inferPaths(files: string[]): string[] {
|
|
55
134
|
// Find common directory prefix
|
|
@@ -3,12 +3,109 @@ import { createHash } from 'node:crypto'
|
|
|
3
3
|
import type { MikkContract, MikkLock } from './schema.js'
|
|
4
4
|
import type { DependencyGraph } from '../graph/types.js'
|
|
5
5
|
import type { ParsedFile } from '../parser/types.js'
|
|
6
|
+
import type { ContextFile } from '../utils/fs.js'
|
|
6
7
|
import { hashContent } from '../hash/file-hasher.js'
|
|
7
8
|
import { computeModuleHash, computeRootHash } from '../hash/tree-hasher.js'
|
|
8
9
|
import { minimatch } from '../utils/minimatch.js'
|
|
9
10
|
|
|
10
11
|
const VERSION = '@getmikk/cli@1.2.1'
|
|
11
12
|
|
|
13
|
+
// ─── Heuristic purpose inference ────────────────────────────────────
|
|
14
|
+
// When JSDoc is missing we derive a short purpose string from:
|
|
15
|
+
// 1. camelCase / PascalCase function name → natural language
|
|
16
|
+
// 2. parameter names (context clue)
|
|
17
|
+
// 3. return type (if present)
|
|
18
|
+
//
|
|
19
|
+
// Examples:
|
|
20
|
+
// "getUserProjectRole" + params:["userId","projectId"] → "Get user project role (userId, projectId)"
|
|
21
|
+
// "DashboardPage" + returnType:"JSX.Element" → "Dashboard page component"
|
|
22
|
+
// ────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/** Split camelCase/PascalCase identifier into lowercase words */
|
|
25
|
+
function splitIdentifier(name: string): string[] {
|
|
26
|
+
return name
|
|
27
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2') // camelCase boundary
|
|
28
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') // ABCDef → ABC Def
|
|
29
|
+
.split(/[\s_-]+/)
|
|
30
|
+
.map(w => w.toLowerCase())
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const JSX_RETURN_TYPES = new Set([
|
|
35
|
+
'jsx.element', 'react.reactnode', 'reactnode', 'react.jsx.element',
|
|
36
|
+
'react.fc', 'reactelement',
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
const HOOK_PREFIXES = ['use']
|
|
40
|
+
const HANDLER_PREFIXES = ['handle', 'on']
|
|
41
|
+
const GETTER_PREFIXES = ['get', 'fetch', 'load', 'find', 'query', 'retrieve', 'read']
|
|
42
|
+
const SETTER_PREFIXES = ['set', 'update', 'save', 'write', 'put', 'patch', 'create', 'delete', 'remove']
|
|
43
|
+
const CHECKER_PREFIXES = ['is', 'has', 'can', 'should', 'check', 'validate']
|
|
44
|
+
|
|
45
|
+
/** Infer a short purpose string from function metadata when JSDoc is missing */
|
|
46
|
+
function inferPurpose(
|
|
47
|
+
name: string,
|
|
48
|
+
params?: { name: string; type?: string }[],
|
|
49
|
+
returnType?: string,
|
|
50
|
+
isAsync?: boolean,
|
|
51
|
+
): string | undefined {
|
|
52
|
+
if (!name) return undefined
|
|
53
|
+
|
|
54
|
+
const words = splitIdentifier(name)
|
|
55
|
+
if (words.length === 0) return undefined
|
|
56
|
+
const firstWord = words[0]
|
|
57
|
+
|
|
58
|
+
// Check if it's a React component (PascalCase + JSX return)
|
|
59
|
+
const isComponent = /^[A-Z]/.test(name) &&
|
|
60
|
+
returnType && JSX_RETURN_TYPES.has(returnType.toLowerCase())
|
|
61
|
+
|
|
62
|
+
if (isComponent) {
|
|
63
|
+
const readable = words.join(' ')
|
|
64
|
+
return capitalise(`${readable} component`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if it's a hook (React, Vue composables, etc.)
|
|
68
|
+
if (HOOK_PREFIXES.includes(firstWord) && words.length > 1) {
|
|
69
|
+
const subject = words.slice(1).join(' ')
|
|
70
|
+
return capitalise(`Hook for ${subject}`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Build base description from name words
|
|
74
|
+
let base: string
|
|
75
|
+
if (HANDLER_PREFIXES.includes(firstWord)) {
|
|
76
|
+
const event = words.slice(1).join(' ')
|
|
77
|
+
base = `Handle ${event}`
|
|
78
|
+
} else if (GETTER_PREFIXES.includes(firstWord)) {
|
|
79
|
+
const subject = words.slice(1).join(' ')
|
|
80
|
+
base = `${capitalise(firstWord)} ${subject}`
|
|
81
|
+
} else if (SETTER_PREFIXES.includes(firstWord)) {
|
|
82
|
+
const subject = words.slice(1).join(' ')
|
|
83
|
+
base = `${capitalise(firstWord)} ${subject}`
|
|
84
|
+
} else if (CHECKER_PREFIXES.includes(firstWord)) {
|
|
85
|
+
const subject = words.slice(1).join(' ')
|
|
86
|
+
base = `Check ${firstWord === 'is' || firstWord === 'has' || firstWord === 'can' ? 'if' : ''} ${subject}`.replace(/ +/g, ' ')
|
|
87
|
+
} else {
|
|
88
|
+
// Generic — just humanise the name
|
|
89
|
+
base = capitalise(words.join(' '))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Append param hint if ≤3 params and they have meaningful names
|
|
93
|
+
if (params && params.length > 0 && params.length <= 3) {
|
|
94
|
+
const meaningful = params
|
|
95
|
+
.map(p => p.name)
|
|
96
|
+
.filter(n => !['e', 'event', 'ctx', 'props', 'args', '_'].includes(n))
|
|
97
|
+
if (meaningful.length > 0) {
|
|
98
|
+
base += ` (${meaningful.join(', ')})`
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return base.trim() || undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function capitalise(s: string): string {
|
|
106
|
+
return s.charAt(0).toUpperCase() + s.slice(1)
|
|
107
|
+
}
|
|
108
|
+
|
|
12
109
|
/**
|
|
13
110
|
* LockCompiler — takes a DependencyGraph and a MikkContract
|
|
14
111
|
* and compiles the complete mikk.lock.json.
|
|
@@ -18,13 +115,15 @@ export class LockCompiler {
|
|
|
18
115
|
compile(
|
|
19
116
|
graph: DependencyGraph,
|
|
20
117
|
contract: MikkContract,
|
|
21
|
-
parsedFiles: ParsedFile[]
|
|
118
|
+
parsedFiles: ParsedFile[],
|
|
119
|
+
contextFiles?: ContextFile[]
|
|
22
120
|
): MikkLock {
|
|
23
121
|
const functions = this.compileFunctions(graph, contract)
|
|
24
122
|
const classes = this.compileClasses(graph, contract)
|
|
25
123
|
const generics = this.compileGenerics(graph, contract)
|
|
26
124
|
const modules = this.compileModules(contract, parsedFiles)
|
|
27
|
-
const files = this.compileFiles(parsedFiles, contract)
|
|
125
|
+
const files = this.compileFiles(parsedFiles, contract, graph)
|
|
126
|
+
const routes = this.compileRoutes(parsedFiles)
|
|
28
127
|
|
|
29
128
|
const moduleHashes: Record<string, string> = {}
|
|
30
129
|
for (const [id, mod] of Object.entries(modules)) {
|
|
@@ -47,6 +146,8 @@ export class LockCompiler {
|
|
|
47
146
|
classes: Object.keys(classes).length > 0 ? classes : undefined,
|
|
48
147
|
generics: Object.keys(generics).length > 0 ? generics : undefined,
|
|
49
148
|
files,
|
|
149
|
+
contextFiles: contextFiles && contextFiles.length > 0 ? contextFiles : undefined,
|
|
150
|
+
routes: routes.length > 0 ? routes : undefined,
|
|
50
151
|
graph: {
|
|
51
152
|
nodes: graph.nodes.size,
|
|
52
153
|
edges: graph.edges.length,
|
|
@@ -90,7 +191,18 @@ export class LockCompiler {
|
|
|
90
191
|
calls: outEdges.filter(e => e.type === 'calls').map(e => e.target),
|
|
91
192
|
calledBy: inEdges.filter(e => e.type === 'calls').map(e => e.source),
|
|
92
193
|
moduleId: moduleId || 'unknown',
|
|
93
|
-
|
|
194
|
+
...(node.metadata.params && node.metadata.params.length > 0
|
|
195
|
+
? { params: node.metadata.params }
|
|
196
|
+
: {}),
|
|
197
|
+
...(node.metadata.returnType ? { returnType: node.metadata.returnType } : {}),
|
|
198
|
+
...(node.metadata.isAsync ? { isAsync: true } : {}),
|
|
199
|
+
...(node.metadata.isExported ? { isExported: true } : {}),
|
|
200
|
+
purpose: node.metadata.purpose || inferPurpose(
|
|
201
|
+
node.label,
|
|
202
|
+
node.metadata.params,
|
|
203
|
+
node.metadata.returnType,
|
|
204
|
+
node.metadata.isAsync,
|
|
205
|
+
),
|
|
94
206
|
edgeCasesHandled: node.metadata.edgeCasesHandled,
|
|
95
207
|
errorHandling: node.metadata.errorHandling,
|
|
96
208
|
detailedLines: node.metadata.detailedLines,
|
|
@@ -116,7 +228,7 @@ export class LockCompiler {
|
|
|
116
228
|
endLine: node.metadata.endLine ?? 0,
|
|
117
229
|
moduleId: moduleId || 'unknown',
|
|
118
230
|
isExported: node.metadata.isExported ?? false,
|
|
119
|
-
purpose: node.metadata.purpose,
|
|
231
|
+
purpose: node.metadata.purpose || inferPurpose(node.label),
|
|
120
232
|
edgeCasesHandled: node.metadata.edgeCasesHandled,
|
|
121
233
|
errorHandling: node.metadata.errorHandling,
|
|
122
234
|
}
|
|
@@ -128,11 +240,14 @@ export class LockCompiler {
|
|
|
128
240
|
graph: DependencyGraph,
|
|
129
241
|
contract: MikkContract
|
|
130
242
|
): Record<string, any> {
|
|
131
|
-
const
|
|
243
|
+
const raw: Record<string, any> = {}
|
|
132
244
|
for (const [id, node] of graph.nodes) {
|
|
133
245
|
if (node.type !== 'generic') continue
|
|
246
|
+
// Only include exported generics — non-exported types/interfaces are
|
|
247
|
+
// internal implementation details that add noise without value.
|
|
248
|
+
if (!node.metadata.isExported) continue
|
|
134
249
|
const moduleId = this.findModule(node.file, contract.declared.modules)
|
|
135
|
-
|
|
250
|
+
raw[id] = {
|
|
136
251
|
id,
|
|
137
252
|
name: node.label,
|
|
138
253
|
type: node.metadata.hash ?? 'generic', // we stored type name in hash
|
|
@@ -141,9 +256,31 @@ export class LockCompiler {
|
|
|
141
256
|
endLine: node.metadata.endLine ?? 0,
|
|
142
257
|
moduleId: moduleId || 'unknown',
|
|
143
258
|
isExported: node.metadata.isExported ?? false,
|
|
144
|
-
purpose: node.metadata.purpose,
|
|
259
|
+
purpose: node.metadata.purpose || inferPurpose(node.label),
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Dedup: group generics with the same name + type that appear in multiple files.
|
|
264
|
+
// Keep the first occurrence and add an `alsoIn` array for the duplicate files.
|
|
265
|
+
const byNameType = new Map<string, { key: string; entry: any; others: string[] }>()
|
|
266
|
+
for (const [key, entry] of Object.entries(raw)) {
|
|
267
|
+
const dedup = `${entry.name}::${entry.type}`
|
|
268
|
+
const existing = byNameType.get(dedup)
|
|
269
|
+
if (existing) {
|
|
270
|
+
existing.others.push(entry.file)
|
|
271
|
+
} else {
|
|
272
|
+
byNameType.set(dedup, { key, entry, others: [] })
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const result: Record<string, any> = {}
|
|
277
|
+
for (const { key, entry, others } of byNameType.values()) {
|
|
278
|
+
if (others.length > 0) {
|
|
279
|
+
entry.alsoIn = others
|
|
145
280
|
}
|
|
281
|
+
result[key] = entry
|
|
146
282
|
}
|
|
283
|
+
|
|
147
284
|
return result
|
|
148
285
|
}
|
|
149
286
|
|
|
@@ -178,23 +315,52 @@ export class LockCompiler {
|
|
|
178
315
|
/** Compile file entries */
|
|
179
316
|
private compileFiles(
|
|
180
317
|
parsedFiles: ParsedFile[],
|
|
181
|
-
contract: MikkContract
|
|
318
|
+
contract: MikkContract,
|
|
319
|
+
graph: DependencyGraph
|
|
182
320
|
): Record<string, MikkLock['files'][string]> {
|
|
183
321
|
const result: Record<string, MikkLock['files'][string]> = {}
|
|
184
322
|
|
|
185
323
|
for (const file of parsedFiles) {
|
|
186
324
|
const moduleId = this.findModule(file.path, contract.declared.modules)
|
|
325
|
+
|
|
326
|
+
// Collect file-level imports from the graph's import edges
|
|
327
|
+
const outEdges = graph.outEdges.get(file.path) || []
|
|
328
|
+
const importedFiles = outEdges
|
|
329
|
+
.filter(e => e.type === 'imports')
|
|
330
|
+
.map(e => e.target)
|
|
331
|
+
|
|
187
332
|
result[file.path] = {
|
|
188
333
|
path: file.path,
|
|
189
334
|
hash: file.hash,
|
|
190
335
|
moduleId: moduleId || 'unknown',
|
|
191
336
|
lastModified: new Date().toISOString(),
|
|
337
|
+
...(importedFiles.length > 0 ? { imports: importedFiles } : {}),
|
|
192
338
|
}
|
|
193
339
|
}
|
|
194
340
|
|
|
195
341
|
return result
|
|
196
342
|
}
|
|
197
343
|
|
|
344
|
+
/** Compile route registrations from all parsed files */
|
|
345
|
+
private compileRoutes(parsedFiles: ParsedFile[]): MikkLock['routes'] & any[] {
|
|
346
|
+
const routes: any[] = []
|
|
347
|
+
for (const file of parsedFiles) {
|
|
348
|
+
if (file.routes && file.routes.length > 0) {
|
|
349
|
+
for (const route of file.routes) {
|
|
350
|
+
routes.push({
|
|
351
|
+
method: route.method,
|
|
352
|
+
path: route.path,
|
|
353
|
+
handler: route.handler,
|
|
354
|
+
middlewares: route.middlewares,
|
|
355
|
+
file: route.file,
|
|
356
|
+
line: route.line,
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return routes
|
|
362
|
+
}
|
|
363
|
+
|
|
198
364
|
/** Find which module a file belongs to based on path patterns */
|
|
199
365
|
private findModule(
|
|
200
366
|
filePath: string,
|