Compare commits

...

2 Commits

Author SHA1 Message Date
snomiao
80e958313d 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>
2025-09-16 13:45:46 +00:00
snomiao
c27d1af2ae feat: Add import map visualization tool
- Add script to generate import dependency map
- Create interactive D3.js visualization
- Add documentation for import map feature
- Add npm script 'pnpm import-map' for easy generation

This helps developers understand module dependencies and
architecture of the codebase through an interactive graph.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 04:25:19 +00:00
10 changed files with 206600 additions and 14 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

132
docs/IMPORT_MAP.md Normal file
View File

@@ -0,0 +1,132 @@
# Import Map Visualization
This document describes the import map visualization tool for the ComfyUI Frontend project.
## Overview
The import map visualization provides an interactive graph showing all the import dependencies in the ComfyUI Frontend codebase. This helps developers understand:
- Module dependencies and relationships
- Code organization and architecture
- Circular dependencies (if any)
- External package usage
- Module coupling and cohesion
## Viewing the Import Map
Open `docs/import-map.html` in a web browser to view the interactive visualization.
### Features
- **Interactive Graph**: Drag nodes to explore the dependency graph
- **Color-Coded Categories**: Different module types are shown in different colors:
- 🔴 Components
- 🔵 Stores
- 🟢 Services
- 🟡 Views
- 🟠 Composables
- ⚪ Utils
- 🟣 External packages
- ⚫ Other modules
- **Search**: Use the search box to find specific files or modules
- **Zoom & Pan**: Navigate through the graph using mouse controls
- **Export**: Export the raw dependency data as JSON
## Generating the Import Map
To regenerate the import map after code changes:
```bash
npx tsx scripts/generate-import-map.ts
```
This will:
1. Scan all TypeScript and Vue files in the `src/` directory
2. Extract import statements
3. Build a dependency graph
4. Generate both JSON data and HTML visualization
### Output Files
- `docs/import-map.json` - Raw dependency data in JSON format
- `docs/import-map.html` - Interactive HTML visualization
## Understanding the Visualization
### Node Size
- Larger nodes indicate modules that are imported by many other modules
- Small nodes are leaf modules with fewer dependents
### Links
- Lines between nodes show import relationships
- Thicker lines indicate multiple imports between the same modules
### Layout
- The graph uses force-directed layout to automatically position nodes
- Highly connected modules tend to cluster together
- External dependencies are typically on the periphery
## Use Cases
### Architecture Review
- Identify architectural patterns and layers
- Spot potential violations of architectural boundaries
- Find opportunities for refactoring
### Dependency Analysis
- Identify heavily used modules that might benefit from optimization
- Find unused or rarely used modules
- Detect circular dependencies
### Onboarding
- Help new developers understand the codebase structure
- Visualize the relationships between different parts of the application
- Identify entry points and core modules
### Performance Optimization
- Find modules that might benefit from code splitting
- Identify heavy external dependencies
- Optimize bundle size by understanding import chains
## Technical Details
The import map generator uses:
- TypeScript AST parsing to extract imports
- D3.js for interactive visualization
- Force-directed graph layout algorithm
- Fast-glob for file system traversal
## Limitations
- Dynamic imports (`import()`) are detected but may not show the full dependency picture
- Conditional imports are shown as always-present dependencies
- Type-only imports are included in the visualization
- The visualization works best with up to ~1000 nodes
## Future Improvements
Potential enhancements for the import map tool:
- [ ] Filter by module type or specific directories
- [ ] Show import cycle detection
- [ ] Display bundle size information
- [ ] Integration with webpack bundle analyzer
- [ ] Real-time updates during development
- [ ] Export to other visualization formats (GraphViz, etc.)
- [ ] Show test file dependencies separately
- [ ] Add metrics dashboard (coupling, cohesion, etc.)
## Contributing
To improve the import map visualization:
1. The generation script is located at `scripts/generate-import-map.ts`
2. The HTML template is embedded in the script
3. Submit PRs with improvements or bug fixes
## Related Documentation
- [Architecture Decision Records](./adr/README.md)
- [Settings System](./SETTINGS.md)
- [Extension Development](./extensions/development.md)

View File

@@ -34,6 +34,7 @@
"locale": "lobe-i18n locale",
"collect-i18n": "npx playwright test --config=playwright.i18n.config.ts",
"json-schema": "tsx scripts/generate-json-schema.ts",
"import-map": "tsx scripts/generate-import-map.ts",
"storybook": "nx storybook -p 6006",
"build-storybook": "storybook build"
},

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

@@ -0,0 +1,807 @@
#!/usr/bin/env tsx
import glob from 'fast-glob'
import fs from 'fs'
import path from 'path'
interface ImportInfo {
source: string
imports: string[]
}
interface DependencyGraph {
nodes: Array<{
id: string
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 }>
}>
}
// Extract imports from a TypeScript/Vue file
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
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
}
}
// 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'
if (relativePath.startsWith('src/services')) return 'services'
if (relativePath.startsWith('src/views')) return 'views'
if (relativePath.startsWith('src/composables')) return 'composables'
if (relativePath.startsWith('src/utils')) return 'utils'
if (relativePath.startsWith('src/types')) return 'types'
if (relativePath.startsWith('src/extensions')) return 'extensions'
if (relativePath.startsWith('src/lib')) return 'lib'
if (relativePath.startsWith('src/scripts')) return 'scripts'
if (relativePath.startsWith('tests')) return 'tests'
if (relativePath.startsWith('browser_tests')) return 'browser_tests'
return 'other'
}
// Resolve import path to actual file
function resolveImportPath(importPath: string, sourceFile: string): string {
// Handle aliases
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'
]
})
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, {
id: sourceId,
label: path.basename(file),
group: getFileGroup(file),
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}`
if (!nodes.has(targetId)) {
nodes.set(targetId, {
id: targetId,
label: importPath,
group: 'external',
size: 1
})
}
} else {
// Try to find the actual file
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)) {
nodes.set(targetId, {
id: targetId,
label: path.basename(actualFile),
group: getFileGroup(actualFile),
size: 1
})
}
} else {
continue // Skip unresolved imports
}
}
// Add link
const linkKey = `${sourceId}->${targetId}`
if (links.has(linkKey)) {
links.get(linkKey)!.value++
} else {
links.set(linkKey, {
source: sourceId,
target: targetId,
value: 1
})
}
// Increase target node size
const targetNode = nodes.get(targetId)
if (targetNode) {
targetNode.size++
}
}
}
// 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()),
circularDependencies: circularDeps
}
}
// Generate HTML visualization
function generateHTML(graph: DependencyGraph): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ComfyUI Frontend Import Map</title>
<script src="https://unpkg.com/d3@7"></script>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a1a;
color: #fff;
}
#container {
display: flex;
height: 100vh;
}
#graph {
flex: 1;
position: relative;
}
#sidebar {
width: 300px;
background: #2a2a2a;
padding: 20px;
overflow-y: auto;
border-left: 1px solid #3a3a3a;
}
h1 {
margin: 0 0 20px 0;
font-size: 1.5em;
color: #fff;
}
.stats {
margin-bottom: 30px;
}
.stat-item {
display: flex;
justify-content: space-between;
margin: 10px 0;
padding: 8px;
background: #1a1a1a;
border-radius: 4px;
}
.legend {
margin-top: 30px;
}
.legend-item {
display: flex;
align-items: center;
margin: 8px 0;
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 10px;
}
.controls {
margin-top: 30px;
}
button {
display: block;
width: 100%;
padding: 10px;
margin: 10px 0;
background: #4a4a4a;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #5a5a5a;
}
.node-tooltip {
position: absolute;
padding: 10px;
background: rgba(0, 0, 0, 0.9);
color: #fff;
border-radius: 4px;
font-size: 12px;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
z-index: 1000;
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 {
width: 100%;
padding: 10px;
margin: 20px 0;
background: #1a1a1a;
color: #fff;
border: 1px solid #3a3a3a;
border-radius: 4px;
font-size: 14px;
}
.highlighted {
stroke: #ff0 !important;
stroke-width: 3px !important;
}
</style>
</head>
<body>
<div id="container">
<div id="graph">
<svg id="svg"></svg>
<div class="node-tooltip"></div>
</div>
<div id="sidebar">
<h1>Import Map</h1>
<div class="stats">
<div class="stat-item">
<span>Total Files:</span>
<span id="total-nodes">${graph.nodes.length}</span>
</div>
<div class="stat-item">
<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">
<div class="legend">
<h3>Categories</h3>
<div class="legend-item">
<div class="legend-color" style="background: #ff6b6b;"></div>
<span>Components</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #4ecdc4;"></div>
<span>Stores</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #45b7d1;"></div>
<span>Services</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #96ceb4;"></div>
<span>Views</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #ffeaa7;"></div>
<span>Composables</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #dfe6e9;"></div>
<span>Utils</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #fab1a0;"></div>
<span>Types</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #a29bfe;"></div>
<span>External</span>
</div>
<div class="legend-item">
<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">
<button onclick="resetZoom()">Reset View</button>
<button onclick="toggleSimulation()">Toggle Physics</button>
<button onclick="exportData()">Export Data</button>
</div>
</div>
</div>
<script>
const graphData = ${JSON.stringify(graph, null, 2)};
// Color scheme for different groups
const colorScale = d3.scaleOrdinal()
.domain(['components', 'stores', 'services', 'views', 'composables', 'utils', 'types', 'external', 'other'])
.range(['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#dfe6e9', '#fab1a0', '#a29bfe', '#636e72']);
// Setup SVG
const width = window.innerWidth - 300;
const height = window.innerHeight;
const svg = d3.select('#svg')
.attr('width', width)
.attr('height', height);
const g = svg.append('g');
// Setup zoom
const zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on('zoom', (event) => {
g.attr('transform', event.transform);
});
svg.call(zoom);
// Create force simulation
const simulation = d3.forceSimulation(graphData.nodes)
.force('link', d3.forceLink(graphData.links)
.id(d => d.id)
.distance(100))
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(d => Math.sqrt(d.size) * 5));
// Create links
const link = g.append('g')
.selectAll('line')
.data(graphData.links)
.enter().append('line')
.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')
.selectAll('circle')
.data(graphData.nodes)
.enter().append('circle')
.attr('r', d => Math.sqrt(d.size) * 3 + 3)
.attr('fill', d => colorScale(d.group))
.attr('stroke', d => d.inCircularDep ? '#ff0000' : '#fff')
.attr('stroke-width', d => d.inCircularDep ? 3 : 1.5)
.call(drag(simulation));
// Add labels for important nodes
const label = g.append('g')
.selectAll('text')
.data(graphData.nodes.filter(d => d.size > 10))
.enter().append('text')
.text(d => d.label)
.style('font-size', '10px')
.style('fill', '#fff')
.attr('dx', 15)
.attr('dy', 4);
// Tooltip
const tooltip = d3.select('.node-tooltip');
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(tooltipContent);
})
.on('mouseout', () => {
tooltip.style('opacity', 0);
});
// Update positions
simulation.on('tick', () => {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node
.attr('cx', d => d.x)
.attr('cy', d => d.y);
label
.attr('x', d => d.x)
.attr('y', d => d.y);
});
// Drag behavior
function drag(simulation) {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
}
// Search functionality
document.getElementById('search').addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
node.classed('highlighted', false);
if (searchTerm) {
node.classed('highlighted', d =>
d.label.toLowerCase().includes(searchTerm) ||
d.id.toLowerCase().includes(searchTerm)
);
}
});
// Control functions
let simulationRunning = true;
function resetZoom() {
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity);
}
function toggleSimulation() {
if (simulationRunning) {
simulation.stop();
} else {
simulation.restart();
}
simulationRunning = !simulationRunning;
}
function exportData() {
const dataStr = JSON.stringify(graphData, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportFileDefaultName = 'import-map.json';
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
}
// Resize handler
window.addEventListener('resize', () => {
const newWidth = window.innerWidth - 300;
const newHeight = window.innerHeight;
svg.attr('width', newWidth).attr('height', newHeight);
simulation.force('center', d3.forceCenter(newWidth / 2, newHeight / 2));
simulation.alpha(0.3).restart();
});
</script>
</body>
</html>`
}
// 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`
)
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(),
'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(),
'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 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()

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"