feat: Enhanced import map with circular dependency detection

- Added circular dependency detection using DFS algorithm
- Nodes in circular deps show red borders
- Links in circular deps show in red color
- Hover tooltips display complete circular import chains
- Added circular dependency counter to stats panel
- Reorganized all import map files to scripts/map/
- Deployed visualization to https://comfyui-frontend-import-map.pages.dev/

Found 140 circular dependencies in the codebase, primarily in:
- litegraph library modules
- Store and service modules
- Widget composables

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
snomiao
2025-09-16 13:45:05 +00:00
parent c27d1af2ae
commit 80e958313d
10 changed files with 205924 additions and 48392 deletions

File diff suppressed because it is too large Load Diff

48347
dist-import-map/index.html Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

155
pnpm-lock.yaml generated
View File

@@ -243,6 +243,9 @@ importers:
'@vue/test-utils':
specifier: ^2.4.6
version: 2.4.6
dependency-cruiser:
specifier: ^17.0.1
version: 17.0.1
eslint:
specifier: ^9.34.0
version: 9.35.0(jiti@2.4.2)
@@ -2903,11 +2906,22 @@ packages:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
acorn-jsx-walk@2.0.0:
resolution: {integrity: sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
acorn-loose@8.5.2:
resolution: {integrity: sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==}
engines: {node: '>=0.4.0'}
acorn-walk@8.3.4:
resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
engines: {node: '>=0.4.0'}
acorn@7.4.1:
resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==}
engines: {node: '>=0.4.0'}
@@ -3309,6 +3323,10 @@ packages:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'}
commander@14.0.1:
resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==}
engines: {node: '>=20'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@@ -3492,6 +3510,11 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dependency-cruiser@17.0.1:
resolution: {integrity: sha512-4clZ8EPsOVoxGA8NMjaE95aJEO118Cd9D7gT5rysx5azij9cPiCSrnjYlZtV+90PFazlD2lZvjzBHkD1ZqGqlw==}
engines: {node: ^20.12||^22||>=24}
hasBin: true
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
@@ -4245,6 +4268,10 @@ packages:
react-devtools-core:
optional: true
interpret@3.1.1:
resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==}
engines: {node: '>=10.13.0'}
is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
@@ -4541,6 +4568,10 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
knip@5.62.0:
resolution: {integrity: sha512-hfTUVzmrMNMT1khlZfAYmBABeehwWUUrizLQoLamoRhSFkygsGIXWx31kaWKBgEaIVL77T3Uz7IxGvSw+CvQ6A==}
engines: {node: '>=18.18.0'}
@@ -4823,6 +4854,10 @@ packages:
media-encoder-host@9.0.20:
resolution: {integrity: sha512-IyEYxw6az97RNuETOAZV4YZqNAPOiF9GKIp5mVZb4HOyWd6mhkWQ34ydOzhqAWogMyc4W05kjN/VCgTtgyFmsw==}
memoize@10.1.0:
resolution: {integrity: sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg==}
engines: {node: '>=18'}
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -5354,6 +5389,10 @@ packages:
promise@7.3.1:
resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
prompts@2.4.2:
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
engines: {node: '>= 6'}
prosemirror-changeset@2.2.1:
resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==}
@@ -5519,6 +5558,10 @@ packages:
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
engines: {node: '>= 4'}
rechoir@0.8.0:
resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==}
engines: {node: '>= 10.13.0'}
recorder-audio-worklet-processor@5.0.35:
resolution: {integrity: sha512-5Nzbk/6QzC3QFQ1EG2SE34c1ygLE22lIOvLyjy7N6XxE/jpAZrL4e7xR+yihiTaG3ajiWy6UjqL4XEBMM9ahFQ==}
@@ -5536,6 +5579,10 @@ packages:
regenerate@1.4.2:
resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
regexp-tree@0.1.27:
resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
hasBin: true
regexpu-core@6.2.0:
resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==}
engines: {node: '>=4'}
@@ -5641,6 +5688,9 @@ packages:
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-regex@2.1.1:
resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@@ -5698,6 +5748,9 @@ packages:
resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==}
engines: {node: '>=18'}
sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
slice-ansi@5.0.0:
resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
engines: {node: '>=12'}
@@ -5977,6 +6030,10 @@ packages:
ts-map@1.0.3:
resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==}
tsconfig-paths-webpack-plugin@4.2.0:
resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==}
engines: {node: '>=10.13.0'}
tsconfig-paths@4.2.0:
resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
engines: {node: '>=6'}
@@ -6291,8 +6348,8 @@ packages:
vue-component-type-helpers@2.2.12:
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
vue-component-type-helpers@3.0.6:
resolution: {integrity: sha512-6CRM8X7EJqWCJOiKPvSLQG+hJPb/Oy2gyJx3pLjUEhY7PuaCthQu3e0zAGI1lqUBobrrk9IT0K8sG2GsCluxoQ==}
vue-component-type-helpers@3.0.7:
resolution: {integrity: sha512-TvyUcFXmjZcXUvU+r1MOyn4/vv4iF+tPwg5Ig33l/FJ3myZkxeQpzzQMLMFWcQAjr6Xs7BRwVy/TwbmNZUA/4w==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
@@ -6373,6 +6430,11 @@ packages:
resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==}
engines: {node: 20 || >=22}
watskeburt@4.2.3:
resolution: {integrity: sha512-uG9qtQYoHqAsnT711nG5iZc/8M5inSmkGCOp7pFaytKG2aTfIca7p//CjiVzAE4P7hzaYuCozMjNNaLgmhbK5g==}
engines: {node: ^18||>=20}
hasBin: true
wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
@@ -8832,7 +8894,7 @@ snapshots:
storybook: 9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
type-fest: 2.19.0
vue: 3.5.13(typescript@5.9.2)
vue-component-type-helpers: 3.0.6
vue-component-type-helpers: 3.0.7
'@tailwindcss/node@4.1.12':
dependencies:
@@ -9658,14 +9720,20 @@ snapshots:
dependencies:
event-target-shim: 5.0.1
acorn-jsx@5.3.2(acorn@8.14.1):
dependencies:
acorn: 8.14.1
acorn-jsx-walk@2.0.0: {}
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.15.0
acorn-loose@8.5.2:
dependencies:
acorn: 8.15.0
acorn-walk@8.3.4:
dependencies:
acorn: 8.15.0
acorn@7.4.1: {}
acorn@8.14.1: {}
@@ -9899,7 +9967,7 @@ snapshots:
dependencies:
ansi-align: 3.0.1
camelcase: 8.0.0
chalk: 5.3.0
chalk: 5.6.0
cli-boxes: 3.0.0
string-width: 7.2.0
type-fest: 4.41.0
@@ -10081,6 +10149,8 @@ snapshots:
commander@13.1.0: {}
commander@14.0.1: {}
commander@2.20.3: {}
commander@8.3.0: {}
@@ -10246,6 +10316,29 @@ snapshots:
delayed-stream@1.0.0: {}
dependency-cruiser@17.0.1:
dependencies:
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
acorn-jsx-walk: 2.0.0
acorn-loose: 8.5.2
acorn-walk: 8.3.4
ajv: 8.17.1
commander: 14.0.1
enhanced-resolve: 5.18.3
ignore: 7.0.5
interpret: 3.1.1
is-installed-globally: 1.0.0
json5: 2.2.3
memoize: 10.1.0
picomatch: 4.0.3
prompts: 2.4.2
rechoir: 0.8.0
safe-regex: 2.1.1
semver: 7.7.2
tsconfig-paths-webpack-plugin: 4.2.0
watskeburt: 4.2.3
dequal@2.0.3: {}
detect-libc@2.0.4: {}
@@ -10575,8 +10668,8 @@ snapshots:
espree@10.2.0:
dependencies:
acorn: 8.14.1
acorn-jsx: 5.3.2(acorn@8.14.1)
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 4.2.1
espree@10.4.0:
@@ -10587,8 +10680,8 @@ snapshots:
espree@9.6.1:
dependencies:
acorn: 8.14.1
acorn-jsx: 5.3.2(acorn@8.14.1)
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 3.4.3
esprima@4.0.1: {}
@@ -11100,6 +11193,8 @@ snapshots:
- bufferutil
- utf-8-validate
interpret@3.1.1: {}
is-arrayish@0.2.1: {}
is-binary-path@2.1.0:
@@ -11377,6 +11472,8 @@ snapshots:
kind-of@6.0.3: {}
kleur@3.0.3: {}
knip@5.62.0(@types/node@20.14.10)(typescript@5.9.2):
dependencies:
'@nodelib/fs.walk': 1.2.8
@@ -11739,6 +11836,10 @@ snapshots:
media-encoder-host-worker: 10.0.19
tslib: 2.8.1
memoize@10.1.0:
dependencies:
mimic-function: 5.0.1
merge-stream@2.0.0: {}
merge2@1.4.1: {}
@@ -12407,6 +12508,11 @@ snapshots:
dependencies:
asap: 2.0.6
prompts@2.4.2:
dependencies:
kleur: 3.0.3
sisteransi: 1.0.5
prosemirror-changeset@2.2.1:
dependencies:
prosemirror-transform: 1.10.2
@@ -12661,6 +12767,10 @@ snapshots:
tiny-invariant: 1.3.3
tslib: 2.8.1
rechoir@0.8.0:
dependencies:
resolve: 1.22.10
recorder-audio-worklet-processor@5.0.35:
dependencies:
'@babel/runtime': 7.27.6
@@ -12688,6 +12798,8 @@ snapshots:
regenerate@1.4.2: {}
regexp-tree@0.1.27: {}
regexpu-core@6.2.0:
dependencies:
regenerate: 1.4.2
@@ -12822,6 +12934,10 @@ snapshots:
safe-buffer@5.2.1: {}
safe-regex@2.1.1:
dependencies:
regexp-tree: 0.1.27
safer-buffer@2.1.2: {}
saxes@6.0.0:
@@ -12872,6 +12988,8 @@ snapshots:
mrmime: 2.0.1
totalist: 3.0.1
sisteransi@1.0.5: {}
slice-ansi@5.0.0:
dependencies:
ansi-styles: 6.2.1
@@ -13066,7 +13184,7 @@ snapshots:
terser@5.39.2:
dependencies:
'@jridgewell/source-map': 0.3.6
acorn: 8.14.1
acorn: 8.15.0
commander: 2.20.3
source-map-support: 0.5.21
@@ -13143,6 +13261,13 @@ snapshots:
ts-map@1.0.3: {}
tsconfig-paths-webpack-plugin@4.2.0:
dependencies:
chalk: 4.1.2
enhanced-resolve: 5.18.3
tapable: 2.2.3
tsconfig-paths: 4.2.0
tsconfig-paths@4.2.0:
dependencies:
json5: 2.2.3
@@ -13278,7 +13403,7 @@ snapshots:
unplugin@1.16.1:
dependencies:
acorn: 8.14.1
acorn: 8.15.0
webpack-virtual-modules: 0.6.2
unplugin@2.3.5:
@@ -13504,7 +13629,7 @@ snapshots:
vue-component-type-helpers@2.2.12: {}
vue-component-type-helpers@3.0.6: {}
vue-component-type-helpers@3.0.7: {}
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
dependencies:
@@ -13588,6 +13713,8 @@ snapshots:
walk-up-path@4.0.0: {}
watskeburt@4.2.3: {}
wcwidth@1.0.1:
dependencies:
defaults: 1.0.4

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1,8 +1,7 @@
#!/usr/bin/env tsx
import glob from 'fast-glob'
import fs from 'fs'
import path from 'path'
import glob from 'fast-glob'
interface ImportInfo {
source: string
@@ -15,11 +14,18 @@ interface DependencyGraph {
label: string
group: string
size: number
inCircularDep?: boolean
circularChains?: string[][]
}>
links: Array<{
source: string
target: string
value: number
isCircular?: boolean
}>
circularDependencies?: Array<{
chain: string[]
edges: Array<{ source: string; target: string }>
}>
}
@@ -27,21 +33,22 @@ interface DependencyGraph {
function extractImports(filePath: string): ImportInfo {
const content = fs.readFileSync(filePath, 'utf-8')
const imports: string[] = []
// Match ES6 import statements
const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"]/g
const importRegex =
/import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"]/g
let match
while ((match = importRegex.exec(content)) !== null) {
imports.push(match[1])
}
// Also match dynamic imports
const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g
while ((match = dynamicImportRegex.exec(content)) !== null) {
imports.push(match[1])
}
return {
source: filePath,
imports: [...new Set(imports)] // Remove duplicates
@@ -51,7 +58,7 @@ function extractImports(filePath: string): ImportInfo {
// Categorize file by its path
function getFileGroup(filePath: string): string {
const relativePath = path.relative(process.cwd(), filePath)
if (relativePath.includes('node_modules')) return 'external'
if (relativePath.startsWith('src/components')) return 'components'
if (relativePath.startsWith('src/stores')) return 'stores'
@@ -65,7 +72,7 @@ function getFileGroup(filePath: string): string {
if (relativePath.startsWith('src/scripts')) return 'scripts'
if (relativePath.startsWith('tests')) return 'tests'
if (relativePath.startsWith('browser_tests')) return 'browser_tests'
return 'other'
}
@@ -75,31 +82,127 @@ function resolveImportPath(importPath: string, sourceFile: string): string {
if (importPath.startsWith('@/')) {
return path.join(process.cwd(), 'src', importPath.slice(2))
}
// Handle relative paths
if (importPath.startsWith('.')) {
const sourceDir = path.dirname(sourceFile)
return path.resolve(sourceDir, importPath)
}
// External module
return importPath
}
// Detect circular dependencies using DFS
function detectCircularDependencies(
nodes: Map<string, any>,
links: Map<string, any>
): Array<{
chain: string[]
edges: Array<{ source: string; target: string }>
}> {
const adjacencyList = new Map<string, Set<string>>()
const circularDeps: Array<{
chain: string[]
edges: Array<{ source: string; target: string }>
}> = []
// Build adjacency list
for (const link of links.values()) {
if (!adjacencyList.has(link.source)) {
adjacencyList.set(link.source, new Set())
}
adjacencyList.get(link.source)!.add(link.target)
}
// DFS to find cycles
const visited = new Set<string>()
const recStack = new Set<string>()
const parent = new Map<string, string>()
function findCycle(node: string, path: string[] = []): void {
visited.add(node)
recStack.add(node)
path.push(node)
const neighbors = adjacencyList.get(node) || new Set()
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
parent.set(neighbor, node)
findCycle(neighbor, [...path])
} else if (recStack.has(neighbor)) {
// Found a cycle
const cycleStartIndex = path.indexOf(neighbor)
if (cycleStartIndex !== -1) {
const chain = path.slice(cycleStartIndex)
chain.push(neighbor) // Complete the cycle
// Create edges for the circular dependency
const edges: Array<{ source: string; target: string }> = []
for (let i = 0; i < chain.length - 1; i++) {
edges.push({ source: chain[i], target: chain[i + 1] })
}
// Check if this cycle is already recorded (avoid duplicates)
const chainStr = [...chain].sort().join('->')
const isNew = !circularDeps.some((dep) => {
const existingChainStr = [...dep.chain].sort().join('->')
return existingChainStr === chainStr
})
if (isNew) {
circularDeps.push({ chain, edges })
}
}
}
}
recStack.delete(node)
}
// Run DFS from each unvisited node
for (const node of nodes.keys()) {
if (!visited.has(node) && !node.startsWith('external:')) {
findCycle(node)
}
}
return circularDeps
}
// Generate dependency graph
async function generateDependencyGraph(): Promise<DependencyGraph> {
const sourceFiles = await glob('src/**/*.{ts,tsx,vue,mts}', {
ignore: ['**/node_modules/**', '**/*.d.ts', '**/*.spec.ts', '**/*.test.ts', '**/*.stories.ts']
ignore: [
'**/node_modules/**',
'**/*.d.ts',
'**/*.spec.ts',
'**/*.test.ts',
'**/*.stories.ts'
]
})
const nodes = new Map<string, { id: string; label: string; group: string; size: number }>()
const links = new Map<string, { source: string; target: string; value: number }>()
const nodes = new Map<
string,
{
id: string
label: string
group: string
size: number
inCircularDep?: boolean
circularChains?: string[][]
}
>()
const links = new Map<
string,
{ source: string; target: string; value: number; isCircular?: boolean }
>()
// Process each file
for (const file of sourceFiles) {
const importInfo = extractImports(file)
const sourceId = path.relative(process.cwd(), file)
// Add source node
if (!nodes.has(sourceId)) {
nodes.set(sourceId, {
@@ -109,12 +212,12 @@ async function generateDependencyGraph(): Promise<DependencyGraph> {
size: 1
})
}
// Process imports
for (const importPath of importInfo.imports) {
const resolvedPath = resolveImportPath(importPath, file)
let targetId: string
// Check if it's an external module
if (!resolvedPath.startsWith('/') && !resolvedPath.startsWith('.')) {
targetId = `external:${importPath}`
@@ -128,16 +231,25 @@ async function generateDependencyGraph(): Promise<DependencyGraph> {
}
} else {
// Try to find the actual file
const possibleExtensions = ['.ts', '.tsx', '.vue', '.mts', '.js', '.json', '/index.ts', '/index.js']
const possibleExtensions = [
'.ts',
'.tsx',
'.vue',
'.mts',
'.js',
'.json',
'/index.ts',
'/index.js'
]
let actualFile = resolvedPath
for (const ext of possibleExtensions) {
if (fs.existsSync(resolvedPath + ext)) {
actualFile = resolvedPath + ext
break
}
}
if (fs.existsSync(actualFile)) {
targetId = path.relative(process.cwd(), actualFile)
if (!nodes.has(targetId)) {
@@ -152,7 +264,7 @@ async function generateDependencyGraph(): Promise<DependencyGraph> {
continue // Skip unresolved imports
}
}
// Add link
const linkKey = `${sourceId}->${targetId}`
if (links.has(linkKey)) {
@@ -164,7 +276,7 @@ async function generateDependencyGraph(): Promise<DependencyGraph> {
value: 1
})
}
// Increase target node size
const targetNode = nodes.get(targetId)
if (targetNode) {
@@ -172,10 +284,48 @@ async function generateDependencyGraph(): Promise<DependencyGraph> {
}
}
}
// Detect circular dependencies
const circularDeps = detectCircularDependencies(nodes, links)
// Mark nodes and links involved in circular dependencies
const nodesInCircularDeps = new Set<string>()
const circularLinkKeys = new Set<string>()
for (const dep of circularDeps) {
// Mark all nodes in the chain
for (const nodeId of dep.chain) {
nodesInCircularDeps.add(nodeId)
const node = nodes.get(nodeId)
if (node) {
node.inCircularDep = true
if (!node.circularChains) {
node.circularChains = []
}
node.circularChains.push(dep.chain)
}
}
// Mark all edges in the chain
for (const edge of dep.edges) {
const linkKey = `${edge.source}->${edge.target}`
circularLinkKeys.add(linkKey)
const link = links.get(linkKey)
if (link) {
link.isCircular = true
}
}
}
console.log(`Found ${circularDeps.length} circular dependencies:`)
circularDeps.forEach((dep, index) => {
console.log(` ${index + 1}. ${dep.chain.join(' → ')}`)
})
return {
nodes: Array.from(nodes.values()),
links: Array.from(links.values())
links: Array.from(links.values()),
circularDependencies: circularDeps
}
}
@@ -282,7 +432,22 @@ function generateHTML(graph: DependencyGraph): string {
opacity: 0;
transition: opacity 0.3s;
z-index: 1000;
max-width: 300px;
max-width: 400px;
}
.circular-dep-warning {
color: #ff6b6b;
font-weight: bold;
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid #444;
}
.circular-chain {
color: #ffa500;
font-family: monospace;
font-size: 11px;
margin-top: 3px;
}
.search-box {
@@ -320,6 +485,10 @@ function generateHTML(graph: DependencyGraph): string {
<span>Total Dependencies:</span>
<span id="total-links">${graph.links.length}</span>
</div>
<div class="stat-item" style="color: #ff6b6b;">
<span>Circular Dependencies:</span>
<span id="circular-deps">${graph.circularDependencies?.length || 0}</span>
</div>
</div>
<input type="text" class="search-box" placeholder="Search files..." id="search">
@@ -362,6 +531,10 @@ function generateHTML(graph: DependencyGraph): string {
<div class="legend-color" style="background: #636e72;"></div>
<span>Other</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: none; border: 2px solid #ff0000;"></div>
<span>Has Circular Dep</span>
</div>
</div>
<div class="controls">
@@ -413,9 +586,9 @@ function generateHTML(graph: DependencyGraph): string {
.selectAll('line')
.data(graphData.links)
.enter().append('line')
.attr('stroke', '#999')
.attr('stroke-opacity', 0.6)
.attr('stroke-width', d => Math.sqrt(d.value));
.attr('stroke', d => d.isCircular ? '#ff6666' : '#999')
.attr('stroke-opacity', d => d.isCircular ? 0.8 : 0.6)
.attr('stroke-width', d => d.isCircular ? Math.sqrt(d.value) * 1.5 : Math.sqrt(d.value));
// Create nodes
const node = g.append('g')
@@ -424,8 +597,8 @@ function generateHTML(graph: DependencyGraph): string {
.enter().append('circle')
.attr('r', d => Math.sqrt(d.size) * 3 + 3)
.attr('fill', d => colorScale(d.group))
.attr('stroke', '#fff')
.attr('stroke-width', 1.5)
.attr('stroke', d => d.inCircularDep ? '#ff0000' : '#fff')
.attr('stroke-width', d => d.inCircularDep ? 3 : 1.5)
.call(drag(simulation));
// Add labels for important nodes
@@ -444,16 +617,40 @@ function generateHTML(graph: DependencyGraph): string {
node.on('mouseover', (event, d) => {
const connections = graphData.links.filter(l => l.source.id === d.id || l.target.id === d.id);
let tooltipContent = \`
<strong>\${d.label}</strong><br>
Type: \${d.group}<br>
Connections: \${connections.length}<br>
Path: \${d.id}
\`;
// Add circular dependency information if applicable
if (d.inCircularDep && d.circularChains) {
tooltipContent += '<div class="circular-dep-warning">⚠️ Circular Dependency Detected!</div>';
d.circularChains.forEach((chain, index) => {
// Only show chains that include this node
if (chain.includes(d.id)) {
// Format the chain to show the cycle clearly
const nodeIndex = chain.indexOf(d.id);
const formattedChain = chain.map((node, i) => {
const basename = node.split('/').pop();
if (i === nodeIndex) {
return \`<strong>\${basename}</strong>\`;
}
return basename;
}).join(' → ');
tooltipContent += \`<div class="circular-chain">Chain \${index + 1}: \${formattedChain}</div>\`;
}
});
}
tooltip
.style('opacity', 1)
.style('left', (event.pageX + 10) + 'px')
.style('top', (event.pageY - 10) + 'px')
.html(\`
<strong>\${d.label}</strong><br>
Type: \${d.group}<br>
Connections: \${connections.length}<br>
Path: \${d.id}
\`);
.html(tooltipContent);
})
.on('mouseout', () => {
tooltip.style('opacity', 0);
@@ -562,29 +759,49 @@ function generateHTML(graph: DependencyGraph): string {
// Main function
async function main() {
console.log('Generating import map...')
try {
const graph = await generateDependencyGraph()
console.log(`Found ${graph.nodes.length} nodes and ${graph.links.length} dependencies`)
console.log(
`Found ${graph.nodes.length} nodes and ${graph.links.length} dependencies`
)
if (graph.circularDependencies && graph.circularDependencies.length > 0) {
console.log(
`\n⚠ Warning: Found ${graph.circularDependencies.length} circular dependencies!`
)
}
// Save JSON data
const jsonPath = path.join(process.cwd(), 'docs', 'import-map.json')
const jsonPath = path.join(
process.cwd(),
'scripts',
'map',
'import-map.json'
)
fs.mkdirSync(path.dirname(jsonPath), { recursive: true })
fs.writeFileSync(jsonPath, JSON.stringify(graph, null, 2))
console.log(`Saved JSON data to ${jsonPath}`)
// Generate and save HTML visualization
const html = generateHTML(graph)
const htmlPath = path.join(process.cwd(), 'docs', 'import-map.html')
const htmlPath = path.join(
process.cwd(),
'scripts',
'map',
'import-map.html'
)
fs.writeFileSync(htmlPath, html)
console.log(`Saved HTML visualization to ${htmlPath}`)
console.log('✅ Import map generation complete!')
console.log('Open docs/import-map.html in a browser to view the visualization')
console.log(
'Open scripts/map/import-map.html in a browser to view the visualization'
)
} catch (error) {
console.error('Error generating import map:', error)
process.exit(1)
}
}
void main()
void main()

48347
scripts/map/import-map.html Normal file

File diff suppressed because it is too large Load Diff

47925
scripts/map/import-map.json Normal file

File diff suppressed because it is too large Load Diff

3
wrangler.toml Normal file
View File

@@ -0,0 +1,3 @@
name = "comfyui-frontend-import-map"
compatibility_date = "2024-09-16"
pages_build_output_dir = "./dist-import-map"