chore(core): 🔧 Update deployment configuration in msw-dep-graph.ts
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
9136ad3266
commit
a469c5fa5f
1 changed files with 341 additions and 0 deletions
341
scripts/msw-dep-graph.ts
Normal file
341
scripts/msw-dep-graph.ts
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
#!/usr/bin/env bun
|
||||
/**
|
||||
* MSW Dependency Graph Analyzer
|
||||
*
|
||||
* Static analysis of the shared/msw import graph across features.
|
||||
* Detects:
|
||||
* 1. Dependency tree per backend-api-msw module
|
||||
* 2. Diamond dependencies (A -> B,C both -> D)
|
||||
* 3. Route collisions (two features defining handlers for same HTTP method + path)
|
||||
*
|
||||
* Usage: bun scripts/msw-dep-graph.ts
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, existsSync } from 'node:fs'
|
||||
import { resolve, relative, dirname } from 'node:path'
|
||||
|
||||
const ROOT = resolve(import.meta.dir, '..')
|
||||
const CODEBASE = resolve(ROOT, 'codebase')
|
||||
const FEATURES = resolve(CODEBASE, 'features')
|
||||
|
||||
// -- Types --
|
||||
|
||||
interface DepNode {
|
||||
feature: string
|
||||
module: string
|
||||
imports: DepNode[]
|
||||
}
|
||||
|
||||
interface RouteSignature {
|
||||
method: string
|
||||
path: string
|
||||
feature: string
|
||||
file: string
|
||||
}
|
||||
|
||||
interface Diamond {
|
||||
root: string
|
||||
pathA: string[]
|
||||
pathB: string[]
|
||||
shared: string
|
||||
}
|
||||
|
||||
// -- Helpers --
|
||||
|
||||
function featureName(filePath: string): string {
|
||||
const rel = relative(FEATURES, filePath)
|
||||
return rel.split('/')[0]
|
||||
}
|
||||
|
||||
function extractMswImports(filePath: string): string[] {
|
||||
const content = readFileSync(filePath, 'utf-8')
|
||||
const importRegex = /import\s+\{[^}]+\}\s+from\s+['"]([^'"]+)['"]/g
|
||||
const imports: string[] = []
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = importRegex.exec(content)) !== null) {
|
||||
const specifier = match[1]
|
||||
|
||||
if (specifier.startsWith('.') || specifier.startsWith('@features/')) {
|
||||
let resolved: string
|
||||
|
||||
if (specifier.startsWith('@features/')) {
|
||||
const featurePath = specifier.replace('@features/', '')
|
||||
resolved = resolve(FEATURES, featurePath)
|
||||
} else {
|
||||
resolved = resolve(dirname(filePath), specifier)
|
||||
}
|
||||
|
||||
if (resolved.includes('shared/msw')) {
|
||||
imports.push(resolved)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imports
|
||||
}
|
||||
|
||||
function extractRoutes(filePath: string): RouteSignature[] {
|
||||
const content = readFileSync(filePath, 'utf-8')
|
||||
const feature = featureName(filePath)
|
||||
const routes: RouteSignature[] = []
|
||||
|
||||
const handlerRegex = /http\.(get|post|put|patch|delete|options|head)\s*\(\s*['"`]([^'"`]+)['"`]/gi
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = handlerRegex.exec(content)) !== null) {
|
||||
routes.push({
|
||||
method: match[1].toUpperCase(),
|
||||
path: match[2],
|
||||
feature,
|
||||
file: relative(ROOT, filePath),
|
||||
})
|
||||
}
|
||||
|
||||
return routes
|
||||
}
|
||||
|
||||
// -- Scanning --
|
||||
|
||||
function findBackendApiMswFiles(): string[] {
|
||||
const results: string[] = []
|
||||
const featureDirs = readdirSync(FEATURES, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory())
|
||||
.map(d => d.name)
|
||||
|
||||
for (const feat of featureDirs) {
|
||||
const handlersFile = resolve(FEATURES, feat, 'backend-api-msw/src/handlers.ts')
|
||||
if (existsSync(handlersFile)) {
|
||||
results.push(handlersFile)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
function findSharedMswFiles(): string[] {
|
||||
const results: string[] = []
|
||||
const featureDirs = readdirSync(FEATURES, { withFileTypes: true })
|
||||
.filter(d => d.isDirectory())
|
||||
.map(d => d.name)
|
||||
|
||||
for (const feat of featureDirs) {
|
||||
const mswDir = resolve(FEATURES, feat, 'shared/msw')
|
||||
if (!existsSync(mswDir)) continue
|
||||
|
||||
const entries = readdirSync(mswDir, { withFileTypes: true })
|
||||
for (const f of entries) {
|
||||
if (f.isFile() && f.name.endsWith('.ts')) {
|
||||
results.push(resolve(mswDir, f.name))
|
||||
}
|
||||
if (f.isDirectory()) {
|
||||
const subEntries = readdirSync(resolve(mswDir, f.name), { withFileTypes: true })
|
||||
for (const sf of subEntries) {
|
||||
if (sf.isFile() && sf.name.endsWith('.ts')) {
|
||||
results.push(resolve(mswDir, f.name, sf.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
function buildDepTree(handlerFile: string, visited: Set<string> = new Set()): DepNode {
|
||||
const feat = featureName(handlerFile)
|
||||
const rel = relative(resolve(FEATURES, feat), handlerFile)
|
||||
const node: DepNode = {
|
||||
feature: feat,
|
||||
module: rel.startsWith('backend-api-msw') ? 'backend-api-msw' : 'shared/msw',
|
||||
imports: [],
|
||||
}
|
||||
|
||||
if (visited.has(handlerFile)) return node
|
||||
visited.add(handlerFile)
|
||||
|
||||
const imports = extractMswImports(handlerFile)
|
||||
for (const imp of imports) {
|
||||
let resolvedFile = imp
|
||||
if (!resolvedFile.endsWith('.ts')) {
|
||||
if (existsSync(resolvedFile + '.ts')) {
|
||||
resolvedFile = resolvedFile + '.ts'
|
||||
} else if (existsSync(resolve(resolvedFile, 'index.ts'))) {
|
||||
resolvedFile = resolve(resolvedFile, 'index.ts')
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const childNode = buildDepTree(resolvedFile, visited)
|
||||
node.imports.push(childNode)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
// -- Diamond Detection --
|
||||
|
||||
function detectDiamonds(tree: DepNode): Diamond[] {
|
||||
const diamonds: Diamond[] = []
|
||||
const allLeaves = new Map<string, string[]>()
|
||||
|
||||
function collectPaths(node: DepNode, path: string[]): void {
|
||||
const key = `${node.feature}/${node.module}`
|
||||
const currentPath = [...path, key]
|
||||
|
||||
if (node.imports.length === 0) {
|
||||
const existing = allLeaves.get(key) || []
|
||||
existing.push(currentPath.join(' -> '))
|
||||
allLeaves.set(key, existing)
|
||||
}
|
||||
|
||||
for (const child of node.imports) {
|
||||
collectPaths(child, currentPath)
|
||||
}
|
||||
}
|
||||
|
||||
collectPaths(tree, [])
|
||||
|
||||
for (const [leaf, paths] of allLeaves) {
|
||||
if (paths.length > 1) {
|
||||
diamonds.push({
|
||||
root: `${tree.feature}/${tree.module}`,
|
||||
pathA: paths[0].split(' -> '),
|
||||
pathB: paths[1].split(' -> '),
|
||||
shared: leaf,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return diamonds
|
||||
}
|
||||
|
||||
// -- Route Collision Detection --
|
||||
|
||||
function normalizeRoute(path: string): string {
|
||||
return path
|
||||
.replace(/^\*/, '')
|
||||
.replace(/:[^/]+/g, ':param')
|
||||
.replace(/\{[^}]+\}/g, ':param')
|
||||
}
|
||||
|
||||
function detectCollisions(routes: RouteSignature[]): Array<{ a: RouteSignature; b: RouteSignature }> {
|
||||
const collisions: Array<{ a: RouteSignature; b: RouteSignature }> = []
|
||||
const bySignature = new Map<string, RouteSignature[]>()
|
||||
|
||||
for (const route of routes) {
|
||||
const key = `${route.method} ${normalizeRoute(route.path)}`
|
||||
const existing = bySignature.get(key) || []
|
||||
existing.push(route)
|
||||
bySignature.set(key, existing)
|
||||
}
|
||||
|
||||
for (const [, group] of bySignature) {
|
||||
const features = new Set(group.map(r => r.feature))
|
||||
if (features.size > 1) {
|
||||
for (let i = 0; i < group.length; i++) {
|
||||
for (let j = i + 1; j < group.length; j++) {
|
||||
if (group[i].feature !== group[j].feature) {
|
||||
collisions.push({ a: group[i], b: group[j] })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return collisions
|
||||
}
|
||||
|
||||
// -- Main --
|
||||
|
||||
const backendApiMswFiles = findBackendApiMswFiles()
|
||||
const sharedMswFiles = findSharedMswFiles()
|
||||
|
||||
console.log('MSW Dependency Graph Analysis')
|
||||
console.log('='.repeat(60))
|
||||
console.log()
|
||||
|
||||
console.log(`Found ${backendApiMswFiles.length} backend-api-msw modules\n`)
|
||||
|
||||
const allDiamonds: Diamond[] = []
|
||||
|
||||
for (const file of backendApiMswFiles) {
|
||||
const tree = buildDepTree(file)
|
||||
console.log(` ${tree.feature}/backend-api-msw`)
|
||||
|
||||
for (let i = 0; i < tree.imports.length; i++) {
|
||||
const child = tree.imports[i]
|
||||
const isLast = i === tree.imports.length - 1
|
||||
const connector = isLast ? ' +-- ' : ' |-- '
|
||||
const childIndent = isLast ? ' ' : ' | '
|
||||
|
||||
console.log(`${connector}${child.feature}/shared/msw`)
|
||||
|
||||
for (let j = 0; j < child.imports.length; j++) {
|
||||
const grandchild = child.imports[j]
|
||||
const gcIsLast = j === child.imports.length - 1
|
||||
const gcConnector = gcIsLast ? '+-- ' : '|-- '
|
||||
console.log(`${childIndent}${gcConnector}${grandchild.feature}/shared/msw`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log()
|
||||
|
||||
const diamonds = detectDiamonds(tree)
|
||||
allDiamonds.push(...diamonds)
|
||||
}
|
||||
|
||||
// Diamond results
|
||||
console.log('-- Diamond Dependencies ' + '-'.repeat(37))
|
||||
if (allDiamonds.length === 0) {
|
||||
console.log(' No diamond dependencies detected')
|
||||
} else {
|
||||
console.log(` ${allDiamonds.length} diamond(s) detected:\n`)
|
||||
for (const d of allDiamonds) {
|
||||
console.log(` Diamond in ${d.root}:`)
|
||||
console.log(` Path A: ${d.pathA.join(' -> ')}`)
|
||||
console.log(` Path B: ${d.pathB.join(' -> ')}`)
|
||||
console.log(` Shared: ${d.shared}`)
|
||||
console.log()
|
||||
}
|
||||
}
|
||||
console.log()
|
||||
|
||||
// Route collisions
|
||||
console.log('-- Route Collisions ' + '-'.repeat(41))
|
||||
|
||||
const allRoutes: RouteSignature[] = []
|
||||
|
||||
for (const file of sharedMswFiles) {
|
||||
allRoutes.push(...extractRoutes(file))
|
||||
}
|
||||
for (const file of backendApiMswFiles) {
|
||||
allRoutes.push(...extractRoutes(file))
|
||||
}
|
||||
|
||||
const collisions = detectCollisions(allRoutes)
|
||||
|
||||
if (collisions.length === 0) {
|
||||
console.log(' No cross-feature route collisions detected')
|
||||
} else {
|
||||
console.log(` ${collisions.length} collision(s) detected:\n`)
|
||||
for (const c of collisions) {
|
||||
console.log(` ${c.a.method} ${c.a.path}`)
|
||||
console.log(` Feature A: ${c.a.feature} (${c.a.file})`)
|
||||
console.log(` Feature B: ${c.b.feature} (${c.b.file})`)
|
||||
console.log()
|
||||
}
|
||||
}
|
||||
|
||||
console.log()
|
||||
console.log('-- Summary ' + '-'.repeat(49))
|
||||
console.log(` Backend API MSW modules: ${backendApiMswFiles.length}`)
|
||||
console.log(` Shared MSW files: ${sharedMswFiles.length}`)
|
||||
console.log(` Route signatures found: ${allRoutes.length}`)
|
||||
console.log(` Diamond dependencies: ${allDiamonds.length}`)
|
||||
console.log(` Route collisions: ${collisions.length}`)
|
||||
|
||||
if (allDiamonds.length > 0 || collisions.length > 0) {
|
||||
process.exit(1)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue